mirror of
https://github.com/rdkit/rdkit.git
synced 2026-06-03 21:44:30 +08:00
* Add RDLog::CaptureLog for capturing log messages Adds an RAII `CaptureLog` class to `namespace RDLog` (alongside the existing `LogStateSetter`) that redirects an RDKit logger's output to an internal `std::stringstream` for the duration of its lifetime. On destruction the original stream destination and enabled state are fully restored. Nesting is supported: an inner capture shadows the outer one and each collects its own messages independently. The default constructor captures `rdErrorLog`; an explicit constructor accepts any `RDLogger`. Both enable the logger if it was previously disabled and restore that state on destruction. Python bindings expose `rdBase.CaptureLog` as a context manager with a `messages` read-only property, mirroring the existing `rdBase.BlockLogs` pattern. Messages remain accessible after the `with` block exits. C++ tests are added to `catch_logs.cpp` (6 Catch2 sections covering basic capture, empty state, enable/restore, stream restore, explicit logger, and nested captures). Python tests are added to `UnitTestLogging.py` (6 unittest cases covering the same scenarios). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * CaptureLog: add per-level properties (error_messages, warning_messages, etc.) The Python CaptureLog wrapper now captures all four log levels simultaneously. Per-level properties (error_messages, warning_messages, info_messages, debug_messages) give access to messages from each logger independently; the existing messages property returns them all combined. The C++ RDLog::CaptureLog class is unchanged — it remains a clean single-logger RAII type. The Python wrapper composes four instances of it, one per log level. Suggested by bp-kelley in PR review. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Refactor CaptureLog: add named per-level subclasses Add CaptureErrorLog, CaptureWarningLog, CaptureInfoLog, and CaptureDebugLog as named convenience subclasses of CaptureLog, each capturing a specific logger. Update Python bindings to expose the four named classes directly (dropping the combined multi-capture approach), and update tests accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Simplify CaptureLog: no argument, captures rdErrorLog only Remove the RDLogger argument overload, the four named subclasses, and the PyCaptureLog template in favor of a single no-argument CaptureLog that mirrors the Schrödinger CaptureRDErrorLog from which it was inspired. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * CaptureLog tests: add dp_dest restoration and LogStateSetter interaction Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Rename CaptureLog to CaptureErrorLog The name CaptureLog was ambiguous; CaptureErrorLog is explicit about which logger it captures and avoids redundancy within namespace RDLog. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Generalize CaptureErrorLog into CaptureLog with logger parameter Replace CaptureErrorLog with CaptureLog, which accepts any RDLogger in its constructor (e.g. rdErrorLog, rdWarningLog). Add CaptureErrorLog as a convenience subclass that pre-fills rdErrorLog, preserving backward compatibility for existing callers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
468 lines
14 KiB
C++
468 lines
14 KiB
C++
|
||
// Copyright (c) 2004-2019 greg Landrum and Rational Discovery LLC
|
||
//
|
||
// @@ All Rights Reserved @@
|
||
// This file is part of the RDKit.
|
||
// The contents are covered by the terms of the BSD license
|
||
// which is included in the file license.txt, found at the root
|
||
// of the RDKit source tree.
|
||
//
|
||
#include <RDBoost/python.h>
|
||
#include <fstream>
|
||
#include <RDBoost/Wrap.h>
|
||
#include <RDBoost/python_streambuf.h>
|
||
#include <RDGeneral/versions.h>
|
||
#include <RDGeneral/Invariant.h>
|
||
#include <cstdlib>
|
||
|
||
#include <RDGeneral/RDLog.h>
|
||
|
||
namespace python = boost::python;
|
||
namespace logging = boost::logging;
|
||
|
||
std::string _version() { return "$Id$"; }
|
||
|
||
// std::ostream wrapper around Python's stderr stream
|
||
struct PyErrStream : std::ostream, std::streambuf {
|
||
static thread_local std::string buffer;
|
||
|
||
PyErrStream() : std::ostream(this) {
|
||
// All done!
|
||
}
|
||
|
||
int overflow(int c) override {
|
||
write(c);
|
||
return 0;
|
||
}
|
||
|
||
void write(char c) {
|
||
if (c == '\n') {
|
||
PyGILStateHolder h;
|
||
PySys_WriteStderr("%s\n", buffer.c_str());
|
||
buffer.clear();
|
||
} else {
|
||
buffer += c;
|
||
}
|
||
}
|
||
};
|
||
|
||
// std::ostream wrapper around Python's logging module
|
||
struct PyLogStream : std::ostream, std::streambuf {
|
||
static thread_local std::string buffer;
|
||
PyObject *logfn = nullptr;
|
||
|
||
PyLogStream(std::string level) : std::ostream(this) {
|
||
PyObject *module = PyImport_ImportModule("logging");
|
||
PyObject *logger = nullptr;
|
||
|
||
if (module != nullptr) {
|
||
logger = PyObject_CallMethod(module, "getLogger", "s", "rdkit");
|
||
Py_DECREF(module);
|
||
}
|
||
|
||
if (logger != nullptr) {
|
||
logfn = PyObject_GetAttrString(logger, level.c_str());
|
||
Py_DECREF(logger);
|
||
}
|
||
|
||
if (PyErr_Occurred()) {
|
||
PyErr_Print();
|
||
}
|
||
}
|
||
|
||
~PyLogStream() override {
|
||
#if PY_VERSION_HEX < 0x30d0000
|
||
if (!_Py_IsFinalizing()) {
|
||
#else
|
||
if (!Py_IsFinalizing()) {
|
||
#endif
|
||
Py_XDECREF(logfn);
|
||
}
|
||
}
|
||
|
||
int overflow(int c) override {
|
||
write(c);
|
||
return 0;
|
||
}
|
||
|
||
void write(char c) {
|
||
if (logfn == nullptr) {
|
||
return;
|
||
}
|
||
|
||
if (c == '\n') {
|
||
PyGILStateHolder h;
|
||
PyObject *result = PyObject_CallFunction(logfn, "s", buffer.c_str());
|
||
Py_XDECREF(result);
|
||
buffer.clear();
|
||
} else {
|
||
buffer += c;
|
||
}
|
||
}
|
||
};
|
||
|
||
// per-thread buffers for the Python loggers
|
||
thread_local std::string PyErrStream::buffer;
|
||
thread_local std::string PyLogStream::buffer;
|
||
|
||
void LogToPythonLogger() {
|
||
static PyLogStream debug("debug");
|
||
static PyLogStream info("info");
|
||
static PyLogStream warning("warning");
|
||
static PyLogStream error("error");
|
||
|
||
rdDebugLog = std::make_shared<logging::rdLogger>(&debug);
|
||
rdInfoLog = std::make_shared<logging::rdLogger>(&info);
|
||
rdWarningLog = std::make_shared<logging::rdLogger>(&warning);
|
||
rdErrorLog = std::make_shared<logging::rdLogger>(&error);
|
||
}
|
||
|
||
void LogToPythonStderr() {
|
||
static PyErrStream debug;
|
||
static PyErrStream info;
|
||
static PyErrStream warning;
|
||
static PyErrStream error;
|
||
|
||
rdDebugLog = std::make_shared<logging::rdLogger>(&debug);
|
||
rdInfoLog = std::make_shared<logging::rdLogger>(&info);
|
||
rdWarningLog = std::make_shared<logging::rdLogger>(&warning);
|
||
rdErrorLog = std::make_shared<logging::rdLogger>(&error);
|
||
}
|
||
|
||
void WrapLogs() {
|
||
static PyErrStream debug; //("RDKit DEBUG: ");
|
||
static PyErrStream error; //("RDKit ERROR: ");
|
||
static PyErrStream warning; //("RDKit WARNING: ");
|
||
static PyErrStream info; //("RDKit INFO: ");
|
||
|
||
if (!rdDebugLog || !rdInfoLog || !rdErrorLog || !rdWarningLog) {
|
||
RDLog::InitLogs();
|
||
}
|
||
|
||
rdDebugLog->SetTee(debug);
|
||
rdInfoLog->SetTee(info);
|
||
rdWarningLog->SetTee(warning);
|
||
rdErrorLog->SetTee(error);
|
||
}
|
||
|
||
void EnableLog(std::string spec) { logging::enable_logs(spec); }
|
||
|
||
void DisableLog(std::string spec) { logging::disable_logs(spec); }
|
||
|
||
std::string LogStatus() { return logging::log_status(); }
|
||
|
||
void AttachFileToLog(std::string spec, std::string filename, int delay = 100) {
|
||
(void)spec;
|
||
(void)filename;
|
||
(void)delay;
|
||
}
|
||
|
||
void LogDebugMsg(const std::string &msg) {
|
||
// NOGIL nogil;
|
||
BOOST_LOG(rdDebugLog) << msg << std::endl;
|
||
}
|
||
|
||
void LogInfoMsg(const std::string &msg) {
|
||
// NOGIL nogil;
|
||
BOOST_LOG(rdInfoLog) << msg << std::endl;
|
||
}
|
||
|
||
void LogWarningMsg(const std::string &msg) {
|
||
// NOGIL nogil;
|
||
BOOST_LOG(rdWarningLog) << msg << std::endl;
|
||
}
|
||
|
||
void LogErrorMsg(const std::string &msg) {
|
||
// NOGIL nogil;
|
||
BOOST_LOG(rdErrorLog) << msg << std::endl;
|
||
}
|
||
|
||
void LogMessage(std::string spec, std::string msg) {
|
||
if (spec == "rdApp.error") {
|
||
LogErrorMsg(msg);
|
||
} else if (spec == "rdApp.warning") {
|
||
LogWarningMsg(msg);
|
||
} else if (spec == "rdApp.info") {
|
||
LogInfoMsg(msg);
|
||
} else if (spec == "rdApp.debug") {
|
||
LogDebugMsg(msg);
|
||
}
|
||
}
|
||
|
||
class BlockLogs : public boost::noncopyable {
|
||
public:
|
||
BlockLogs() : m_log_setter{new RDLog::LogStateSetter} {}
|
||
~BlockLogs() = default;
|
||
|
||
BlockLogs *enter() { return this; }
|
||
|
||
void exit(python::object exc_type, python::object exc_val,
|
||
python::object traceback) {
|
||
RDUNUSED_PARAM(exc_type);
|
||
RDUNUSED_PARAM(exc_val);
|
||
RDUNUSED_PARAM(traceback);
|
||
m_log_setter.reset();
|
||
}
|
||
|
||
private:
|
||
std::unique_ptr<RDLog::LogStateSetter> m_log_setter;
|
||
};
|
||
|
||
struct PyCaptureErrorLog : boost::noncopyable {
|
||
PyCaptureErrorLog *enter() { return this; }
|
||
void exit(python::object /*exc_type*/, python::object /*exc_val*/,
|
||
python::object /*traceback*/) {
|
||
if (m_capturer) {
|
||
m_messages = m_capturer->messages();
|
||
m_capturer.reset();
|
||
}
|
||
}
|
||
std::string messages() const {
|
||
return m_capturer ? m_capturer->messages() : m_messages;
|
||
}
|
||
|
||
private:
|
||
std::unique_ptr<RDLog::CaptureErrorLog> m_capturer{
|
||
new RDLog::CaptureErrorLog};
|
||
std::string m_messages;
|
||
};
|
||
|
||
namespace {
|
||
struct python_streambuf_wrapper {
|
||
typedef boost_adaptbx::python::streambuf wt;
|
||
|
||
static void wrap() {
|
||
using namespace boost::python;
|
||
class_<wt, boost::noncopyable>("streambuf", no_init)
|
||
.def(init<object &, std::size_t>(
|
||
(arg("python_file_obj"), arg("buffer_size") = 0),
|
||
"documentation")[with_custodian_and_ward_postcall<0, 2>()]);
|
||
}
|
||
};
|
||
|
||
struct python_ostream_wrapper {
|
||
typedef boost_adaptbx::python::ostream wt;
|
||
|
||
static void wrap() {
|
||
using namespace boost::python;
|
||
class_<std::ostream, boost::noncopyable>("std_ostream", no_init);
|
||
class_<wt, boost::noncopyable, bases<std::ostream>>("ostream", no_init)
|
||
.def(init<object &, std::size_t>(
|
||
(arg("python_file_obj"), arg("buffer_size") = 0)));
|
||
}
|
||
};
|
||
|
||
void seedRNG(unsigned int seed) { std::srand(seed); }
|
||
|
||
/// Simple Boost.Python custom converter from pathlib.Path to std::string
|
||
template <typename T = std::string>
|
||
struct path_converter {
|
||
path_converter() {
|
||
python::converter::registry::push_back(&path_converter::convertible,
|
||
&path_converter::construct,
|
||
boost::python::type_id<T>());
|
||
}
|
||
|
||
/// Check PyObject is a pathlib.Path
|
||
static void *convertible(PyObject *object) {
|
||
// paranoia
|
||
if (object == nullptr) {
|
||
return nullptr;
|
||
}
|
||
python::object boost_object(python::handle<>(python::borrowed(object)));
|
||
|
||
std::string object_classname = boost::python::extract<std::string>(
|
||
boost_object.attr("__class__").attr("__name__"));
|
||
// pathlib.Path is always specialized to the below derived classes
|
||
if (object_classname == "WindowsPath" || object_classname == "PosixPath") {
|
||
return object;
|
||
}
|
||
|
||
return nullptr;
|
||
}
|
||
|
||
/// Construct a std::string from pathlib.Path using its own __str__ attribute
|
||
static void construct(
|
||
PyObject *object,
|
||
boost::python::converter::rvalue_from_python_stage1_data *data) {
|
||
void *storage =
|
||
((boost::python::converter::rvalue_from_python_storage<T> *)data)
|
||
->storage.bytes;
|
||
python::object boost_object{python::handle<>{python::borrowed(object)}};
|
||
new (storage)
|
||
T{boost::python::extract<std::string>{boost_object.attr("__str__")()}};
|
||
data->convertible = storage;
|
||
}
|
||
};
|
||
|
||
/// Convert a Python str to a std::string_view
|
||
template <typename T = std::string_view>
|
||
struct string_view_from_python_converter {
|
||
string_view_from_python_converter() {
|
||
python::converter::registry::push_back(
|
||
&string_view_from_python_converter::convertible,
|
||
&string_view_from_python_converter::construct, python::type_id<T>());
|
||
}
|
||
|
||
/// Check PyObject is a str
|
||
static void *convertible(PyObject *object) {
|
||
if (object != nullptr && PyUnicode_Check(object)) {
|
||
return object;
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
/// Construct a std::string_view from the internal string of the PyObject.
|
||
/// This shouldn´t fail, as we block the Python thread until the C++ code
|
||
/// returns, so the PyObject and the internal string should remain valid.
|
||
static void construct(
|
||
PyObject *object,
|
||
python::converter::rvalue_from_python_stage1_data *data) {
|
||
const char *tmp = PyUnicode_AsUTF8(object);
|
||
|
||
void *storage = ((python::converter::rvalue_from_python_storage<T> *)data)
|
||
->storage.bytes;
|
||
new (storage) T{tmp};
|
||
data->convertible = storage;
|
||
}
|
||
};
|
||
|
||
struct string_view_to_python_converter {
|
||
static PyObject *convert(const std::string_view &s) {
|
||
return python::incref(python::str(s.data(), s.size()).ptr());
|
||
}
|
||
};
|
||
|
||
} // namespace
|
||
|
||
BOOST_PYTHON_MODULE(rdBase) {
|
||
python::scope().attr("__doc__") =
|
||
"Module containing basic definitions for wrapped C++ code\n"
|
||
"\n";
|
||
RDLog::InitLogs();
|
||
RegisterVectorConverter<int>();
|
||
RegisterVectorConverter<unsigned>();
|
||
RegisterVectorConverter<size_t>("UnsignedLong_Vect");
|
||
RegisterVectorConverter<boost::uint64_t>("VectSizeT");
|
||
|
||
RegisterVectorConverter<double>();
|
||
RegisterVectorConverter<std::string>(1);
|
||
RegisterVectorConverter<std::vector<int>>();
|
||
RegisterVectorConverter<std::vector<unsigned>>();
|
||
RegisterVectorConverter<std::vector<double>>();
|
||
RegisterVectorConverter<std::vector<std::string>>("VectorOfStringVectors");
|
||
|
||
RegisterVectorConverter<std::pair<int, int>>("MatchTypeVect");
|
||
|
||
path_converter();
|
||
string_view_from_python_converter();
|
||
|
||
RegisterListConverter<int>();
|
||
RegisterListConverter<std::vector<int>>();
|
||
RegisterListConverter<std::vector<unsigned int>>();
|
||
|
||
python::register_exception_translator<IndexErrorException>(
|
||
&translate_index_error);
|
||
python::register_exception_translator<ValueErrorException>(
|
||
&translate_value_error);
|
||
python::register_exception_translator<KeyErrorException>(
|
||
&translate_key_error);
|
||
|
||
#if INVARIANT_EXCEPTION_METHOD
|
||
python::register_exception_translator<Invar::Invariant>(
|
||
&translate_invariant_error);
|
||
#endif
|
||
|
||
boost::python::to_python_converter<std::string_view,
|
||
string_view_to_python_converter>();
|
||
|
||
python::def("_version", _version,
|
||
"Deprecated, use the constant rdkitVersion instead");
|
||
|
||
python::scope().attr("rdkitVersion") = RDKit::rdkitVersion;
|
||
python::scope().attr("boostVersion") = RDKit::boostVersion;
|
||
python::scope().attr("rdkitBuild") = RDKit::rdkitBuild;
|
||
|
||
python::scope().attr("_serializationEnabled") =
|
||
#ifdef RDK_USE_BOOST_SERIALIZATION
|
||
true;
|
||
#else
|
||
false;
|
||
#endif
|
||
python::scope().attr("_iostreamsEnabled") =
|
||
#ifdef RDK_USE_BOOST_IOSTREAMS
|
||
true;
|
||
#else
|
||
false;
|
||
#endif
|
||
python::scope().attr("_multithreadedEnabled") =
|
||
#ifdef RDK_BUILD_THREADSAFE_SSS
|
||
true;
|
||
#else
|
||
false;
|
||
#endif
|
||
|
||
python::def("LogToCppStreams", RDLog::InitLogs,
|
||
"Initialize RDKit logs with C++ streams");
|
||
python::def("LogToPythonLogger", LogToPythonLogger,
|
||
"Initialize RDKit logs with Python's logging module");
|
||
python::def("LogToPythonStderr", LogToPythonStderr,
|
||
"Initialize RDKit logs with Python's stderr stream");
|
||
python::def("WrapLogs", WrapLogs,
|
||
"Tee the RDKit logs to Python's stderr stream");
|
||
|
||
python::def("EnableLog", EnableLog, python::args("spec"));
|
||
python::def("DisableLog", DisableLog, python::args("spec"));
|
||
python::def("LogStatus", LogStatus);
|
||
|
||
python::def("LogDebugMsg", LogDebugMsg, python::args("msg"),
|
||
"Log a message to the RDKit debug logs");
|
||
python::def("LogInfoMsg", LogInfoMsg, python::args("msg"),
|
||
"Log a message to the RDKit info logs");
|
||
python::def("LogWarningMsg", LogWarningMsg, python::args("msg"),
|
||
"Log a message to the RDKit warning logs");
|
||
python::def("LogErrorMsg", LogErrorMsg, python::args("msg"),
|
||
"Log a message to the RDKit error logs");
|
||
python::def("LogMessage", LogMessage, python::args("spec", "msg"),
|
||
"Log a message to any rdApp.* log");
|
||
|
||
python::def("AttachFileToLog", AttachFileToLog,
|
||
"Causes the log to write to a file",
|
||
(python::arg("spec"), python::arg("filename"),
|
||
python::arg("delay") = 100));
|
||
|
||
python::def("SeedRandomNumberGenerator", seedRNG,
|
||
"Provides a seed to the standard C random number generator\n"
|
||
"This does not affect pure Python code, but is relevant to some "
|
||
"of the RDKit C++ components.",
|
||
(python::arg("seed")));
|
||
|
||
python_streambuf_wrapper::wrap();
|
||
python_ostream_wrapper::wrap();
|
||
|
||
python::class_<BlockLogs, boost::noncopyable>(
|
||
"BlockLogs",
|
||
"Temporarily block logs from outputting while this instance is in scope.",
|
||
python::init<>(python::args("self")))
|
||
.def("__enter__", &BlockLogs::enter,
|
||
python::return_internal_reference<>())
|
||
.def("__exit__", &BlockLogs::exit);
|
||
|
||
python::class_<PyCaptureErrorLog, boost::noncopyable>(
|
||
"CaptureErrorLog",
|
||
"Captures messages from rdErrorLog while this instance is in scope.\n"
|
||
"Can be used as a context manager. The ``messages`` property is\n"
|
||
"accessible both inside the context and after it exits.\n"
|
||
"Nesting is supported: inner captures shadow outer ones.\n\n"
|
||
"Example::\n\n"
|
||
" with rdBase.CaptureErrorLog() as capture:\n"
|
||
" rdkit_function_that_may_fail()\n"
|
||
" print(capture.messages)\n",
|
||
python::init<>(python::args("self")))
|
||
.def("__enter__", &PyCaptureErrorLog::enter,
|
||
python::return_internal_reference<>())
|
||
.def("__exit__", &PyCaptureErrorLog::exit)
|
||
.add_property("messages", &PyCaptureErrorLog::messages,
|
||
"Messages captured from rdErrorLog.");
|
||
}
|