RDKit
Open-source cheminformatics and machine learning.
Loading...
Searching...
No Matches
python_streambuf_nb.h
Go to the documentation of this file.
1//
2// This file is part of the CCTBX distribution:
3// http://cctbx.sourceforge.net/
4// Downloaded from here:
5// http://cctbx.svn.sourceforge.net/viewvc/cctbx/trunk/boost_adaptbx/python_streambuf.h?revision=13619
6//
7// Copyright (c) 2006, The Regents of the University of
8// California, through Lawrence Berkeley National Laboratory (subject to
9// receipt of any required approvals from the U.S. Dept. of Energy). All
10// rights reserved.
11//
12// The license is here:
13// http://cctbx.svn.sourceforge.net/viewvc/cctbx/trunk/boost_adaptbx/LICENSE_2_0.txt?revision=5148
14//
15#include <RDGeneral/export.h>
16
17#ifndef BOOST_ADAPTBX_PYTHON_STREAMBUF_H
18#define BOOST_ADAPTBX_PYTHON_STREAMBUF_H
19
20#include <RDGeneral/Invariant.h>
22#include <optional>
23#include <nanobind/nanobind.h>
24namespace nb = nanobind;
25using namespace nb::literals;
26
27#include <streambuf>
28
29namespace boost_adaptbx {
30namespace python {
31
32/// A stream buffer getting data from and putting data into a Python file object
33/** The aims are as follow:
34
35 - Given a C++ function acting on a standard stream, e.g.
36
37 \code
38 void read_inputs(std::istream& input) {
39 ...
40 input >> something >> something_else;
41 }
42 \endcode
43
44 and given a piece of Python code which creates a file-like object,
45 to be able to pass this file object to that C++ function, e.g.
46
47 \code
48 import gzip
49 gzip_file_obj = gzip.GzipFile(...)
50 read_inputs(gzip_file_obj)
51 \endcode
52
53 and have the standard stream pull data from and put data into the Python
54 file object.
55
56 - When Python \c read_inputs() returns, the Python object is able to
57 continue reading or writing where the C++ code left off.
58
59 - Operations in C++ on mere files should be competitively fast compared
60 to the direct use of \c std::fstream.
61
62
63 \b Motivation
64
65 - the standard Python library offer of file-like objects (files,
66 compressed files and archives, network, ...) is far superior to the
67 offer of streams in the C++ standard library and Boost C++ libraries.
68
69 - i/o code involves a fair amount of text processing which is more
70 efficiently prototyped in Python but then one may need to rewrite
71 a time-critical part in C++, in as seamless a manner as possible.
72
73 \b Usage
74
75 This is 2-step:
76
77 - a trivial wrapper function
78
79 \code
80 using boost_adaptbx::python::streambuf;
81 void read_inputs_wrapper(streambuf& input)
82 {
83 streambuf::istream is(input);
84 read_inputs(is);
85 }
86
87 def("read_inputs", read_inputs_wrapper);
88 \endcode
89
90 which has to be written every time one wants a Python binding for
91 such a C++ function.
92
93 - the Python side
94
95 \code
96 from boost.python import streambuf
97 read_inputs(streambuf(python_file_obj=obj, buffer_size=1024))
98 \endcode
99
100 \c buffer_size is optional. See also: \c default_buffer_size
101
102 Note: references are to the C++ standard (the numbers between parentheses
103 at the end of references are margin markers).
104*/
105class streambuf : public std::basic_streambuf<char> {
106 private:
107 typedef std::basic_streambuf<char> base_t;
108
109 public:
110 /* The syntax
111 using base_t::char_type;
112 would be nicer but Visual Studio C++ 8 chokes on it
113 */
114 typedef base_t::char_type char_type;
115 typedef base_t::int_type int_type;
116 typedef base_t::pos_type pos_type;
117 typedef base_t::off_type off_type;
118 typedef base_t::traits_type traits_type;
119
120 // work around Visual C++ 7.1 problem
121 inline static int traits_type_eof() { return traits_type::eof(); }
122
123 /// The default size of the read and write buffer.
124 /** They are respectively used to buffer data read from and data written to
125 the Python file object. It can be modified from Python.
126 */
127 const static std::size_t default_buffer_size = 1024;
128
129 /// Construct from a Python file object
130 /** if buffer_size is 0 the current default_buffer_size is used.
131 */
132 streambuf(nb::object &python_file_obj, std::size_t buffer_size_ = 0)
133 : py_read(nb::getattr(python_file_obj, "read", nb::none())),
134 py_write(nb::getattr(python_file_obj, "write", nb::none())),
135 py_seek(nb::getattr(python_file_obj, "seek", nb::none())),
136 py_tell(nb::getattr(python_file_obj, "tell", nb::none())),
137 buffer_size(buffer_size_ != 0 ? buffer_size_ : default_buffer_size),
138 write_buffer(nullptr),
139 pos_of_read_buffer_end_in_py_file(0),
140 pos_of_write_buffer_end_in_py_file(buffer_size),
141 farthest_pptr(nullptr) {
142 TEST_ASSERT(buffer_size != 0);
143 /* Some Python file objects (e.g. sys.stdout and sys.stdin)
144 have non-functional seek and tell. If so, assign None to
145 py_tell and py_seek.
146 */
147 if (!py_tell.is_none()) {
148 try {
149 off_type py_pos = nb::cast<off_type>(py_tell());
150 if (!py_seek.is_none()) {
151 /* Make sure we can actually seek.
152 bzip2 readers from python have a seek method, but it fails
153 when they are in write mode.
154 */
155 py_seek(py_pos);
156 }
157 } catch (nb::python_error &) {
158 py_tell = nb::none();
159 py_seek = nb::none();
160 /* Boost.Python does not do any Python exception handling whatsoever
161 So we need to catch it by hand like so.
162 */
163 PyErr_Clear();
164 }
165 }
166
167 if (!py_write.is_none()) {
168 // C-like string to make debugging easier
169 write_buffer = new char[buffer_size + 1];
170 write_buffer[buffer_size] = '\0';
171 setp(write_buffer, write_buffer + buffer_size); // 27.5.2.4.5 (5)
172 farthest_pptr = pptr();
173 } else {
174 // The first attempt at output will result in a call to overflow
175 setp(nullptr, nullptr);
176 }
177
178 if (!py_tell.is_none()) {
179 off_type py_pos = nb::cast<off_type>(py_tell());
180 pos_of_read_buffer_end_in_py_file = py_pos;
181 pos_of_write_buffer_end_in_py_file = py_pos;
182 }
183 }
184
185 /// constructor to enforce a mode (binary or text)
186 streambuf(nb::object &python_file_obj, char mode,
187 std::size_t buffer_size_ = 0)
188 : streambuf(python_file_obj, buffer_size_) {
189#if 1
190 auto io_mod = nb::module_::import_("io");
191 CHECK_INVARIANT(io_mod, "module not found");
192 nb::object iobase = nb::getattr(io_mod, "TextIOBase");
193 CHECK_INVARIANT(iobase, "base class not found");
194#else
195 // using statics to save an undetermined amount of time results in
196 // alarming seg faults on windows. so we don't do it. Keep this here
197 // for the moment though in case someone manages to figure that out in
198 // the future
199 static nb::object io_mod = nb::object();
200 static nb::object iobase = nb::object();
201 if (!io_mod) io_mod = nb::import("io");
202 if (io_mod && !iobase) iobase = nb::getattr(io_mod, "TextIOBase");
203 CHECK_INVARIANT(io_mod, "module not found");
204 CHECK_INVARIANT(iobase, "base class not found");
205#endif
206
207 df_isTextMode = PyObject_IsInstance(python_file_obj.ptr(), iobase.ptr());
208 switch (mode) {
209 case 's': /// yeah, is redundant, but it is somehow natural to do "s"
210 case 't':
211 if (!df_isTextMode) {
213 "Need a text mode file object like StringIO or a file opened "
214 "with mode 't'");
215 }
216 break;
217 case 'b':
218 if (df_isTextMode) {
220 "Need a binary mode file object like BytesIO or a file opened "
221 "with mode 'b'");
222 }
223 break;
224 default:
225 throw std::invalid_argument("bad mode character");
226 }
227 }
228
229 /// Mundane destructor freeing the allocated resources
230 ~streambuf() override {
231 if (write_buffer) {
232 delete[] write_buffer;
233 }
234 }
235
236 /// C.f. C++ standard section 27.5.2.4.3
237 /** It is essential to override this virtual function for the stream
238 member function readsome to work correctly (c.f. 27.6.1.3, alinea 30)
239 */
240 std::streamsize showmanyc() override {
241 int_type const failure = traits_type::eof();
242 int_type status = underflow();
243 if (status == failure) {
244 return -1;
245 }
246 return egptr() - gptr();
247 }
248
249 /// C.f. C++ standard section 27.5.2.4.3
250 int_type underflow() override {
251 int_type const failure = traits_type::eof();
252 if (py_read.is_none()) {
253 throw std::invalid_argument(
254 "That Python file object has no 'read' attribute");
255 }
256 read_buffer = py_read(buffer_size);
257 char *read_buffer_data;
258 Py_ssize_t py_n_read;
259 if (PyBytes_AsStringAndSize(read_buffer.ptr(), &read_buffer_data,
260 &py_n_read) == -1) {
261 setg(nullptr, nullptr, nullptr);
262 throw std::invalid_argument(
263 "The method 'read' of the Python file object "
264 "did not return a string.");
265 }
266 off_type n_read = (off_type)py_n_read;
267 pos_of_read_buffer_end_in_py_file += n_read;
268 setg(read_buffer_data, read_buffer_data, read_buffer_data + n_read);
269 // ^^^27.5.2.3.1 (4)
270 if (n_read == 0) {
271 return failure;
272 }
273 return traits_type::to_int_type(read_buffer_data[0]);
274 }
275
276 /// C.f. C++ standard section 27.5.2.4.5
278 if (py_write.is_none()) {
279 throw std::invalid_argument(
280 "That Python file object has no 'write' attribute");
281 }
282 farthest_pptr = std::max(farthest_pptr, pptr());
283 off_type n_written = (off_type)(farthest_pptr - pbase());
284 off_type orig_n_written = n_written;
285 const unsigned int STD_ASCII = 0x7F;
286 if (df_isTextMode && static_cast<unsigned int>(c) > STD_ASCII) {
287 // we're somewhere in the middle of a utf8 block. If we
288 // only write part of it we'll end up with an exception,
289 // so push everything that could be utf8 into the next block
290 while (n_written > 0 && static_cast<unsigned int>(
291 write_buffer[n_written - 1]) > STD_ASCII) {
292 --n_written;
293 }
294 }
295 nb::str chunk(pbase(), n_written);
296 py_write(chunk);
297
298 if ((!df_isTextMode || static_cast<unsigned int>(c) <= STD_ASCII) &&
299 !traits_type::eq_int_type(c, traits_type::eof())) {
300 py_write(traits_type::to_char_type(c));
301 n_written++;
302 }
303
304 setp(pbase(), epptr());
305 // ^^^ 27.5.2.4.5 (5)
306 farthest_pptr = pptr();
307 if (n_written) {
308 pos_of_write_buffer_end_in_py_file += n_written;
309 if (df_isTextMode && static_cast<unsigned int>(c) > STD_ASCII &&
310 !traits_type::eq_int_type(c, traits_type::eof())) {
311 size_t n_to_copy = orig_n_written - n_written;
312
313 for (size_t i = 0; i < n_to_copy; ++i) {
314 sputc(write_buffer[n_written + i]);
315 ++farthest_pptr;
316 }
317 sputc(c);
318 ++farthest_pptr;
319 }
320 }
321 return traits_type::eq_int_type(c, traits_type::eof())
322 ? traits_type::not_eof(c)
323 : c;
324 }
325
326 /// Update the python file to reflect the state of this stream buffer
327 /** Empty the write buffer into the Python file object and set the seek
328 position of the latter accordingly (C++ standard section 27.5.2.4.2).
329 If there is no write buffer or it is empty, but there is a non-empty
330 read buffer, set the Python file object seek position to the
331 seek position in that read buffer.
332 */
333 int sync() override {
334 int result = 0;
335 farthest_pptr = std::max(farthest_pptr, pptr());
336 if (farthest_pptr && farthest_pptr > pbase()) {
337 off_type delta = pptr() - farthest_pptr;
338 int_type status = overflow();
339 if (traits_type::eq_int_type(status, traits_type::eof())) {
340 result = -1;
341 }
342 if (!py_seek.is_none()) {
343 py_seek(delta, 1);
344 }
345 } else if (gptr() && gptr() < egptr()) {
346 if (!py_seek.is_none()) {
347 py_seek(gptr() - egptr(), 1);
348 }
349 }
350 return result;
351 }
352
353 /// C.f. C++ standard section 27.5.2.4.2
354 /** This implementation is optimised to look whether the position is within
355 the buffers, so as to avoid calling Python seek or tell. It is
356 important for many applications that the overhead of calling into Python
357 is avoided as much as possible (e.g. parsers which may do a lot of
358 backtracking)
359 */
360 pos_type seekoff(off_type off, std::ios_base::seekdir way,
361 std::ios_base::openmode which =
362 std::ios_base::in | std::ios_base::out) override {
363 /* In practice, "which" is either std::ios_base::in or out
364 since we end up here because either seekp or seekg was called
365 on the stream using this buffer. That simplifies the code
366 in a few places.
367 */
368 int const failure = off_type(-1);
369
370 if (py_seek.is_none()) {
371 throw std::invalid_argument(
372 "That Python file object has no 'seek' attribute");
373 }
374
375 // we need the read buffer to contain something!
376 if (which == std::ios_base::in && !gptr()) {
377 if (traits_type::eq_int_type(underflow(), traits_type::eof())) {
378 return failure;
379 }
380 }
381
382 // compute the whence parameter for Python seek
383 int whence;
384 switch (way) {
385 case std::ios_base::beg:
386 whence = 0;
387 break;
388 case std::ios_base::cur:
389 whence = 1;
390 break;
391 case std::ios_base::end:
392 whence = 2;
393 break;
394 default:
395 return failure;
396 }
397
398 // Let's have a go
399 std::optional<off_type> result =
400 seekoff_without_calling_python(off, way, which);
401 if (!result) {
402 // we need to call Python
403 if (which == std::ios_base::out) {
404 overflow();
405 }
406 if (way == std::ios_base::cur) {
407 if (which == std::ios_base::in) {
408 off -= egptr() - gptr();
409 } else if (which == std::ios_base::out) {
410 off += pptr() - pbase();
411 }
412 }
413 py_seek(off, whence);
414 result = off_type(nb::cast<off_type>(py_tell()));
415 if (which == std::ios_base::in) {
416 underflow();
417 }
418 }
419 return *result;
420 }
421
422 /// C.f. C++ standard section 27.5.2.4.2
424 std::ios_base::openmode which =
425 std::ios_base::in | std::ios_base::out) override {
426 return streambuf::seekoff(sp, std::ios_base::beg, which);
427 }
428
429 private:
430 nb::object py_read, py_write, py_seek, py_tell;
431
432 std::size_t buffer_size;
433
434 /* This is actually a Python string and the actual read buffer is
435 its internal data, i.e. an array of characters. We use a Boost.Python
436 object so as to hold on it: as a result, the actual buffer can't
437 go away.
438 */
439 nb::object read_buffer;
440
441 /* A mere array of char's allocated on the heap at construction time and
442 de-allocated only at destruction time.
443 */
444 char *write_buffer;
445 bool df_isTextMode;
446
447 off_type pos_of_read_buffer_end_in_py_file,
448 pos_of_write_buffer_end_in_py_file;
449
450 // the farthest place the buffer has been written into
451 char *farthest_pptr;
452
453 std::optional<off_type> seekoff_without_calling_python(
454 off_type off, std::ios_base::seekdir way, std::ios_base::openmode which) {
455 std::optional<off_type> const failure = off_type(-1);
456
457 // Buffer range and current position
458 off_type buf_begin, buf_end, buf_cur, upper_bound;
459 off_type pos_of_buffer_end_in_py_file;
460 if (which == std::ios_base::in) {
461 pos_of_buffer_end_in_py_file = pos_of_read_buffer_end_in_py_file;
462 buf_begin = reinterpret_cast<std::streamsize>(eback());
463 buf_cur = reinterpret_cast<std::streamsize>(gptr());
464 buf_end = reinterpret_cast<std::streamsize>(egptr());
465 upper_bound = buf_end;
466 } else if (which == std::ios_base::out) {
467 pos_of_buffer_end_in_py_file = pos_of_write_buffer_end_in_py_file;
468 buf_begin = reinterpret_cast<std::streamsize>(pbase());
469 buf_cur = reinterpret_cast<std::streamsize>(pptr());
470 buf_end = reinterpret_cast<std::streamsize>(epptr());
471 farthest_pptr = std::max(farthest_pptr, pptr());
472 upper_bound = reinterpret_cast<std::streamsize>(farthest_pptr) + 1;
473 } else {
474 CHECK_INVARIANT(0, "unreachable code");
475 }
476
477 // Sought position in "buffer coordinate"
478 off_type buf_sought;
479 if (way == std::ios_base::cur) {
480 buf_sought = buf_cur + off;
481 } else if (way == std::ios_base::beg) {
482 buf_sought = buf_end + (off - pos_of_buffer_end_in_py_file);
483 } else if (way == std::ios_base::end) {
484 return failure;
485 } else {
486 CHECK_INVARIANT(0, "unreachable code");
487 }
488
489 // if the sought position is not in the buffer, give up
490 if (buf_sought < buf_begin || buf_sought >= upper_bound) {
491 return failure;
492 }
493
494 // we are in wonderland
495 if (which == std::ios_base::in) {
496 gbump(buf_sought - buf_cur);
497 } else if (which == std::ios_base::out) {
498 pbump(buf_sought - buf_cur);
499 }
500 return pos_of_buffer_end_in_py_file + (buf_sought - buf_end);
501 }
502
503 public:
504 class istream : public std::istream {
505 public:
506 istream(streambuf &buf) : std::istream(&buf) {
507 exceptions(std::ios_base::badbit);
508 }
509
510 ~istream() override {
511 // do nothing.
512 // This used to do:
513 // if (this->good()) this->sync();
514 // but that caused problems if the underlying file had been closed
515 // (see github #579) and really doesn't seem necessary for what we're
516 // doing.
517 }
518 };
519
520 class ostream : public std::ostream {
521 public:
522 ostream(streambuf &buf) : std::ostream(&buf) {
523 exceptions(std::ios_base::badbit);
524 }
525
526 // overload that takes ownership of the streambuf ptr
527 ostream(streambuf *buf) : std::ostream(buf), m_buf(buf) {
528 exceptions(std::ios_base::badbit);
529 }
530
531 ~ostream() override {
532 if (this->good()) {
533 this->flush();
534 }
535 delete m_buf;
536 }
537
538 private:
539 streambuf *m_buf = nullptr;
540 };
541};
542
543// std::size_t streambuf::default_buffer_size = 1024;
544
545struct streambuf_capsule {
546 streambuf python_streambuf;
547
548 streambuf_capsule(nb::object &python_file_obj, std::size_t buffer_size = 0)
549 : python_streambuf(python_file_obj, buffer_size) {}
550};
551
552struct ostream : private streambuf_capsule, streambuf::ostream {
553 ostream(nb::object &python_file_obj, std::size_t buffer_size = 0)
554 : streambuf_capsule(python_file_obj, buffer_size),
556
557 ~ostream() noexcept override {
558 if (this->good()) {
559 this->flush();
560 }
561 }
562};
563} // namespace python
564} // namespace boost_adaptbx
565
566#endif // GUARD
#define TEST_ASSERT(expr)
Definition Invariant.h:151
#define CHECK_INVARIANT(expr, mess)
Definition Invariant.h:100
Class to allow us to throw a ValueError from C++ and have it make it back to Python.
Definition Exceptions.h:41
A stream buffer getting data from and putting data into a Python file object.
~streambuf() override
Mundane destructor freeing the allocated resources.
streambuf(nb::object &python_file_obj, char mode, std::size_t buffer_size_=0)
constructor to enforce a mode (binary or text)
static const std::size_t default_buffer_size
The default size of the read and write buffer.
pos_type seekpos(pos_type sp, std::ios_base::openmode which=std::ios_base::in|std::ios_base::out) override
C.f. C++ standard section 27.5.2.4.2.
std::streamsize showmanyc() override
C.f. C++ standard section 27.5.2.4.3.
pos_type seekoff(off_type off, std::ios_base::seekdir way, std::ios_base::openmode which=std::ios_base::in|std::ios_base::out) override
C.f. C++ standard section 27.5.2.4.2.
streambuf(nb::object &python_file_obj, std::size_t buffer_size_=0)
Construct from a Python file object.
int sync() override
Update the python file to reflect the state of this stream buffer.
int_type overflow(int_type c=traits_type_eof()) override
C.f. C++ standard section 27.5.2.4.5.
int_type underflow() override
C.f. C++ standard section 27.5.2.4.3.
streambuf(bp::object &python_file_obj, std::size_t buffer_size_=0)
Construct from a Python file object.
ostream(bp::object &python_file_obj, std::size_t buffer_size=0)
ostream(nb::object &python_file_obj, std::size_t buffer_size=0)
streambuf_capsule(bp::object &python_file_obj, std::size_t buffer_size=0)
streambuf_capsule(nb::object &python_file_obj, std::size_t buffer_size=0)