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()