Skip to content

Stdout redirection #1009

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

Merged
merged 7 commits into from
Aug 25, 2017
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
66 changes: 66 additions & 0 deletions docs/advanced/pycpp/utilities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,72 @@ expected in Python:
auto args = py::make_tuple("unpacked", true);
py::print("->", *args, "end"_a="<-"); // -> unpacked True <-

.. _ostream_redirect:

Capturing standard output from ostream
======================================

Often, a library will use the streams ``std::cout`` and ``std::cerr`` to print,
but this does not play well with Python's standard ``sys.stdout`` and ``sys.stderr``
redirection. Replacing a library's printing with `py::print <print>` may not
be feasible. This can be fixed using a guard around the library function that
redirects output to the corresponding Python streams:

.. code-block:: cpp

#include <pybind11/iostream.h>

...

// Add a scoped redirect for your noisy code
m.def("noisy_func", []() {
py::scoped_ostream_redirect stream(
std::cout, // std::ostream&
py::module::import("sys").attr("stdout") // Python output
);
call_noisy_func();
});

This method respects flushes on the output streams and will flush if needed
when the scoped guard is destroyed. This allows the output to be redirected in
real time, such as to a Jupyter notebook. The two arguments, the C++ stream and
the Python output, are optional, and default to standard output if not given. An
extra type, `py::scoped_estream_redirect <scoped_estream_redirect>`, is identical
except for defaulting to ``std::cerr`` and ``sys.stderr``; this can be useful with
`py::call_guard`, which allows multiple items, but uses the default constructor:

.. code-block:: py

// Alternative: Call single function using call guard
m.def("noisy_func", &call_noisy_function,
py::call_guard<py::scoped_ostream_redirect,
py::scoped_estream_redirect>());

The redirection can also be done in Python with the addition of a context
manager, using the `py::add_ostream_redirect() <add_ostream_redirect>` function:

.. code-block:: cpp

py::add_ostream_redirect(m, "ostream_redirect");

The name in Python defaults to ``ostream_redirect`` if no name is passed. This
creates the following context manager in Python:

.. code-block:: python

with ostream_redirect(stdout=True, stderr=True):
noisy_function()

It defaults to redirecting both streams, though you can use the keyword
arguments to disable one of the streams if needed.

.. note::

The above methods will not redirect C-level output to file descriptors, such
as ``fprintf``. For those cases, you'll need to redirect the file
descriptors either directly in C or with Python's ``os.dup2`` function
in an operating-system dependent way.

.. _eval:

Evaluating Python expressions from strings and files
Expand Down
10 changes: 7 additions & 3 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,15 @@ v2.2.0 (Not yet released)
7. Fixed lifetime of temporary C++ objects created in Python-to-C++ conversions.
`#924 <https://github.com/pybind/pybind11/pull/924>`_.

* Scope guard call policy for RAII types, e.g. ``py::call_guard<py::gil_scoped_release>()``.
See :ref:`call_policies` for details.
* Scope guard call policy for RAII types, e.g. ``py::call_guard<py::gil_scoped_release>()``,
``py::call_guard<py::scoped_ostream_redirect>()``. See :ref:`call_policies` for details.
`#740 <https://github.com/pybind/pybind11/pull/740>`_.

* Utility for redirecting C++ streams to Python (e.g. ``std::cout`` ->
``sys.stdout``). Scope guard ``py::scoped_ostream_redirect`` in C++ and
a context manager in Python. See :ref:`ostream_redirect`.
`#1009 <https://github.com/pybind/pybind11/pull/1009>`_.

* Improved handling of types and exceptions across module boundaries.
`#915 <https://github.com/pybind/pybind11/pull/915>`_,
`#951 <https://github.com/pybind/pybind11/pull/951>`_,
Expand Down Expand Up @@ -298,7 +303,6 @@ v2.2.0 (Not yet released)
`#923 <https://github.com/pybind/pybind11/pull/923>`_,
`#963 <https://github.com/pybind/pybind11/pull/963>`_.


v2.1.1 (April 7, 2017)
-----------------------------------------------------

Expand Down
9 changes: 9 additions & 0 deletions docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ Embedding the interpreter

.. doxygenclass:: scoped_interpreter

Redirecting C++ streams
=======================

.. doxygenclass:: scoped_ostream_redirect

.. doxygenclass:: scoped_estream_redirect

.. doxygenfunction:: add_ostream_redirect

Python build-in functions
=========================

Expand Down
200 changes: 200 additions & 0 deletions include/pybind11/iostream.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
pybind11/iostream.h -- Tools to assist with redirecting cout and cerr to Python

Copyright (c) 2017 Henry F. Schreiner

All rights reserved. Use of this source code is governed by a
BSD-style license that can be found in the LICENSE file.
*/

#pragma once

#include "pybind11.h"

#include <streambuf>
#include <ostream>
#include <string>
#include <memory>
#include <iostream>

NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
NAMESPACE_BEGIN(detail)

// Buffer that writes to Python instead of C++
class pythonbuf : public std::streambuf {
private:
using traits_type = std::streambuf::traits_type;

char d_buffer[1024];
object pywrite;
object pyflush;

int overflow(int c) {
if (!traits_type::eq_int_type(c, traits_type::eof())) {
*pptr() = traits_type::to_char_type(c);
pbump(1);
}
return sync() ? traits_type::not_eof(c) : traits_type::eof();
}

int sync() {
if (pbase() != pptr()) {
// This subtraction cannot be negative, so dropping the sign
str line(pbase(), static_cast<size_t>(pptr() - pbase()));

pywrite(line);
pyflush();

setp(pbase(), epptr());
}
return 0;
}

public:
pythonbuf(object pyostream)
: pywrite(pyostream.attr("write")),
pyflush(pyostream.attr("flush")) {
setp(d_buffer, d_buffer + sizeof(d_buffer) - 1);
}

/// Sync before destroy
~pythonbuf() {
sync();
}
};

NAMESPACE_END(detail)


/** \rst
This a move-only guard that redirects output.

.. code-block:: cpp

#include <pybind11/iostream.h>

...

{
py::scoped_ostream_redirect output;
std::cout << "Hello, World!"; // Python stdout
} // <-- return std::cout to normal

You can explicitly pass the c++ stream and the python object,
for example to guard stderr instead.

.. code-block:: cpp

{
py::scoped_ostream_redirect output{std::cerr, py::module::import("sys").attr("stderr")};
std::cerr << "Hello, World!";
}
\endrst */
class scoped_ostream_redirect {
protected:
std::streambuf *old;
std::ostream &costream;
detail::pythonbuf buffer;

public:
scoped_ostream_redirect(
std::ostream &costream = std::cout,
object pyostream = module::import("sys").attr("stdout"))
: costream(costream), buffer(pyostream) {
old = costream.rdbuf(&buffer);
}

~scoped_ostream_redirect() {
costream.rdbuf(old);
}

scoped_ostream_redirect(const scoped_ostream_redirect &) = delete;
scoped_ostream_redirect(scoped_ostream_redirect &&other) = default;
scoped_ostream_redirect &operator=(const scoped_ostream_redirect &) = delete;
scoped_ostream_redirect &operator=(scoped_ostream_redirect &&) = delete;
};


/** \rst
Like `scoped_ostream_redirect`, but redirects cerr by default. This class
is provided primary to make ``py::call_guard`` easier to make.

.. code-block:: cpp

m.def("noisy_func", &noisy_func,
py::call_guard<scoped_ostream_redirect,
scoped_estream_redirect>());

\endrst */
class scoped_estream_redirect : public scoped_ostream_redirect {
public:
scoped_estream_redirect(
std::ostream &costream = std::cerr,
object pyostream = module::import("sys").attr("stderr"))
: scoped_ostream_redirect(costream,pyostream) {}
};


NAMESPACE_BEGIN(detail)

// Class to redirect output as a context manager. C++ backend.
class OstreamRedirect {
bool do_stdout_;
bool do_stderr_;
std::unique_ptr<scoped_ostream_redirect> redirect_stdout;
std::unique_ptr<scoped_estream_redirect> redirect_stderr;

public:
OstreamRedirect(bool do_stdout = true, bool do_stderr = true)
: do_stdout_(do_stdout), do_stderr_(do_stderr) {}

void enter() {
if (do_stdout_)
redirect_stdout.reset(new scoped_ostream_redirect());
if (do_stderr_)
redirect_stderr.reset(new scoped_estream_redirect());
}

void exit() {
redirect_stdout.reset();
redirect_stderr.reset();
}
};

NAMESPACE_END(detail)

/** \rst
This is a helper function to add a C++ redirect context manager to Python
instead of using a C++ guard. To use it, add the following to your binding code:

.. code-block:: cpp

#include <pybind11/iostream.h>

...

py::add_ostream_redirect(m, "ostream_redirect");

You now have a Python context manager that redirects your output:

.. code-block:: python

with m.ostream_redirect():
m.print_to_cout_function()

This manager can optionally be told which streams to operate on:

.. code-block:: python

with m.ostream_redirect(stdout=true, stderr=true):
m.noisy_function_with_error_printing()

\endrst */
inline class_<detail::OstreamRedirect> add_ostream_redirect(module m, std::string name = "ostream_redirect") {
return class_<detail::OstreamRedirect>(m, name.c_str(), module_local())
.def(init<bool,bool>(), arg("stdout")=true, arg("stderr")=true)
.def("__enter__", &detail::OstreamRedirect::enter)
.def("__exit__", [](detail::OstreamRedirect &self, args) { self.exit(); });
}

NAMESPACE_END(PYBIND11_NAMESPACE)
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
'include/pybind11/embed.h',
'include/pybind11/eval.h',
'include/pybind11/functional.h',
'include/pybind11/iostream.h',
'include/pybind11/numpy.h',
'include/pybind11/operators.h',
'include/pybind11/options.h',
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ set(PYBIND11_TEST_FILES
test_eval.cpp
test_exceptions.cpp
test_factory_constructors.cpp
test_iostream.cpp
test_kwargs_and_defaults.cpp
test_local_bindings.cpp
test_methods_and_attributes.cpp
Expand Down
Loading