Add Read() to PythonFileObjectAdapter.
PiperOrigin-RevId: 268666385
diff --git a/python/cc/clif/python_file_object_adapter.clif b/python/cc/clif/python_file_object_adapter.clif
index 2ded31c..00d931e 100644
--- a/python/cc/clif/python_file_object_adapter.clif
+++ b/python/cc/clif/python_file_object_adapter.clif
@@ -20,3 +20,6 @@
@virtual
def `Close` as close(self) -> Status
+
+ @virtual
+ def `Read` as read(self, size: int) -> StatusOr<bytes>
diff --git a/python/cc/python_file_object_adapter.h b/python/cc/python_file_object_adapter.h
index 342e05e..84e8b8f 100644
--- a/python/cc/python_file_object_adapter.h
+++ b/python/cc/python_file_object_adapter.h
@@ -26,13 +26,17 @@
// This is CLIFed and implemented in Python.
class PythonFileObjectAdapter {
public:
- // Writes 'data' to the underlying stream and returns the number of bytes
- // written, which can be less than the size of 'data'.
+ // Writes 'data' to the underlying Python file object and returns the number
+ // of bytes written, which can be less than the size of 'data'.
virtual util::StatusOr<int> Write(absl::string_view data) = 0;
- // Closes the underlying stream.
+ // Closes the underlying Python file object.
virtual util::Status Close() = 0;
+ // Reads at most 'size' bytes from the underlying Python file object. Returns
+ // OUT_OF_RANGE status if the file object is alreday at EOF.
+ virtual util::StatusOr<std::string> Read(int size) = 0;
+
virtual ~PythonFileObjectAdapter() {}
};
diff --git a/python/cc/python_output_stream_test.cc b/python/cc/python_output_stream_test.cc
index a8ec476..1b277b0 100644
--- a/python/cc/python_output_stream_test.cc
+++ b/python/cc/python_output_stream_test.cc
@@ -58,7 +58,7 @@
for (size_t stream_size : {0, 10, 100, 1000, 10000, 100000, 1000000}) {
SCOPED_TRACE(absl::StrCat("stream_size = ", stream_size));
std::string stream_contents = subtle::Random::GetRandomBytes(stream_size);
- auto output = absl::make_unique<test::TestPythonFileObjectAdapter>();
+ auto output = absl::make_unique<test::TestWritableObject>();
std::string* output_buffer = output->GetBuffer();
auto output_stream =
absl::make_unique<PythonOutputStream>(std::move(output));
@@ -74,7 +74,7 @@
std::string stream_contents = subtle::Random::GetRandomBytes(stream_size);
for (int buffer_size : {1, 10, 100, 1000, 10000, 100000, 1000000}) {
SCOPED_TRACE(absl::StrCat("buffer_size = ", buffer_size));
- auto output = absl::make_unique<test::TestPythonFileObjectAdapter>();
+ auto output = absl::make_unique<test::TestWritableObject>();
std::string* output_buffer = output->GetBuffer();
auto output_stream =
absl::make_unique<PythonOutputStream>(std::move(output), buffer_size);
@@ -95,7 +95,7 @@
int buffer_size = 1234;
void* buffer;
std::string stream_contents = subtle::Random::GetRandomBytes(stream_size);
- auto output = absl::make_unique<test::TestPythonFileObjectAdapter>();
+ auto output = absl::make_unique<test::TestWritableObject>();
std::string* output_buffer = output->GetBuffer();
// Prepare the stream and do the first call to Next().
diff --git a/python/cc/test_util.h b/python/cc/test_util.h
index b8933c9..eb0d49e 100644
--- a/python/cc/test_util.h
+++ b/python/cc/test_util.h
@@ -22,8 +22,8 @@
namespace tink {
namespace test {
-// PythonFileObjectAdapter for testing.
-class TestPythonFileObjectAdapter : public PythonFileObjectAdapter {
+// Writable PythonFileObjectAdapter for testing.
+class TestWritableObject : public PythonFileObjectAdapter {
public:
util::StatusOr<int> Write(absl::string_view data) override {
buffer_ += std::string(data);
@@ -32,6 +32,10 @@
util::Status Close() override { return util::OkStatus(); }
+ util::StatusOr<std::string> Read(int size) override {
+ return util::Status(util::error::UNIMPLEMENTED, "not readable");
+ }
+
std::string* GetBuffer() { return &buffer_; }
private:
diff --git a/python/util/file_object_adapter.py b/python/util/file_object_adapter.py
index 47461e5..5b275fb 100644
--- a/python/util/file_object_adapter.py
+++ b/python/util/file_object_adapter.py
@@ -11,8 +11,8 @@
# limitations under the License.
"""FileObjectAdapter class.
-Used in conjunction with PythonOutputStream to allow a C++ OutputStream
-to write to a Python file-like object.
+Used in conjunction with PythonOutputStream/PythonInputStream to allow a C++
+OutputStream/InputStream to interact with a Python file-like object.
"""
from __future__ import absolute_import
@@ -30,8 +30,6 @@
"""Adapts a Python file object for use in C++."""
def __init__(self, file_object: BinaryIO):
- if not file_object.writable():
- raise TypeError('File object must be writable.')
self._file_object = file_object
def write(self, data: bytes) -> int:
@@ -44,3 +42,29 @@
def close(self) -> None:
self._file_object.close()
+
+ def read(self, size: int) -> bytes:
+ """Reads at most 'size' bytes from the underlying file object.
+
+ Args:
+ size: A non-negative integer, maximum number of bytes to read.
+
+ Returns:
+ Bytes that were read. An empty bytes object is returned if no bytes are
+ available at the moment.
+
+ Raises:
+ EOFError if the file object is already at EOF.
+ """
+ if size < 0:
+ raise ValueError('size must be non-negative')
+
+ try:
+ data = self._file_object.read(size)
+ if data is None:
+ return b''
+ elif not data and size > 0:
+ raise EOFError('EOF')
+ return data
+ except io.BlockingIOError:
+ return b''
diff --git a/python/util/file_object_adapter_test.py b/python/util/file_object_adapter_test.py
index 15ff5c3..9965767 100644
--- a/python/util/file_object_adapter_test.py
+++ b/python/util/file_object_adapter_test.py
@@ -24,9 +24,10 @@
class FileObjectAdapterTest(absltest.TestCase):
- def test_basic(self):
+ def test_basic_write(self):
file_object = io.BytesIO()
adapter = file_object_adapter.FileObjectAdapter(file_object)
+
self.assertEqual(9, adapter.write(b'something'))
self.assertEqual(b'something', file_object.getvalue())
adapter.close()
@@ -34,49 +35,81 @@
def test_multiple_write(self):
file_object = io.BytesIO()
adapter = file_object_adapter.FileObjectAdapter(file_object)
+
self.assertEqual(9, adapter.write(b'something'))
self.assertEqual(3, adapter.write(b'123'))
self.assertEqual(3, adapter.write(b'456'))
self.assertEqual(b'something123456', file_object.getvalue())
- adapter.close()
def test_write_after_close(self):
file_object = io.BytesIO()
adapter = file_object_adapter.FileObjectAdapter(file_object)
+
adapter.close()
self.assertRaises(ValueError, adapter.write, b'something')
- def test_non_writable(self):
- file_object = mock.Mock()
- file_object.writable = mock.Mock(return_value=False)
-
- self.assertRaises(TypeError, file_object_adapter.FileObjectAdapter,
- file_object)
-
def test_write_returns_none(self):
file_object = mock.Mock()
- file_object.writable = mock.Mock(return_value=True)
file_object.write = mock.Mock(return_value=None)
-
adapter = file_object_adapter.FileObjectAdapter(file_object)
+
self.assertEqual(0, adapter.write(b'something'))
def test_write_raises_blocking_error(self):
file_object = mock.Mock()
- file_object.writable = mock.Mock(return_value=True)
file_object.write = mock.Mock(side_effect=io.BlockingIOError(None, None, 5))
-
adapter = file_object_adapter.FileObjectAdapter(file_object)
+
self.assertEqual(5, adapter.write(b'something'))
def test_partial_write(self):
file_object = mock.Mock()
- file_object.writable = mock.Mock(return_value=True)
file_object.write = mock.Mock(wraps=lambda data: len(data) - 1)
-
adapter = file_object_adapter.FileObjectAdapter(file_object)
+
self.assertEqual(8, adapter.write(b'something'))
+ def test_basic_read(self):
+ file_object = io.BytesIO(b'something')
+ adapter = file_object_adapter.FileObjectAdapter(file_object)
+
+ self.assertEqual(adapter.read(9), b'something')
+
+ def test_multiple_read(self):
+ file_object = io.BytesIO(b'something')
+ adapter = file_object_adapter.FileObjectAdapter(file_object)
+
+ self.assertEqual(adapter.read(3), b'som')
+ self.assertEqual(adapter.read(3), b'eth')
+ self.assertEqual(adapter.read(3), b'ing')
+
+ def test_read_returns_none(self):
+ file_object = mock.Mock()
+ file_object.read = mock.Mock(return_value=None)
+ adapter = file_object_adapter.FileObjectAdapter(file_object)
+
+ self.assertEqual(adapter.read(10), b'')
+
+ def test_read_eof(self):
+ file_object = mock.Mock()
+ file_object.read = mock.Mock(return_value=b'')
+ adapter = file_object_adapter.FileObjectAdapter(file_object)
+
+ self.assertRaises(EOFError, adapter.read, 10)
+
+ def test_read_size_0(self):
+ file_object = io.BytesIO(b'something')
+ adapter = file_object_adapter.FileObjectAdapter(file_object)
+
+ self.assertEqual(adapter.read(0), b'')
+
+ def test_read_raises_blocking_error(self):
+ file_object = mock.Mock()
+ file_object.read = mock.Mock(side_effect=io.BlockingIOError(None, None))
+ adapter = file_object_adapter.FileObjectAdapter(file_object)
+
+ self.assertEqual(adapter.read(10), b'')
+
if __name__ == '__main__':
absltest.main()