Skip to content

Commit

Permalink
feat(bindings/python): add sync File.readline (#5271)
Browse files Browse the repository at this point in the history
  • Loading branch information
TennyZhuang authored Nov 2, 2024
1 parent 8e142c8 commit aa94e83
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 1 deletion.
1 change: 1 addition & 0 deletions bindings/python/python/opendal/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ class AsyncOperator:
@final
class File:
def read(self, size: Optional[int] = None) -> bytes: ...
def readline(self, size: Optional[int] = None) -> bytes: ...
def write(self, bs: bytes) -> None: ...
def seek(self, pos: int, whence: int = 0) -> int: ...
def tell(self) -> int: ...
Expand Down
46 changes: 46 additions & 0 deletions bindings/python/src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
// Remove this `allow` after <https://github.com/rust-lang/rust-clippy/issues/12039> fixed.
#![allow(clippy::unnecessary_fallible_conversions)]

use std::io::BufRead;
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
Expand Down Expand Up @@ -97,6 +98,51 @@ impl File {
Buffer::new(buffer).into_bytes_ref(py)
}

/// Read a single line from the file.
/// A newline character (`\n`) is left at the end of the string, and is only omitted on the last line of the file if the file doesn’t end in a newline.
/// If size is specified, at most size bytes will be read.
#[pyo3(signature = (size=None,))]
pub fn readline<'p>(
&'p mut self,
py: Python<'p>,
size: Option<usize>,
) -> PyResult<Bound<PyAny>> {
let reader = match &mut self.0 {
FileState::Reader(r) => r,
FileState::Writer(_) => {
return Err(PyIOError::new_err(
"I/O operation failed for reading on write only file.",
));
}
FileState::Closed => {
return Err(PyIOError::new_err(
"I/O operation failed for reading on closed file.",
));
}
};

let buffer = match size {
None => {
let mut buffer = Vec::new();
reader
.read_until(b'\n', &mut buffer)
.map_err(|err| PyIOError::new_err(err.to_string()))?;
buffer
}
Some(size) => {
let mut bs = vec![0; size];
let mut reader = reader.take(size as u64);
let n = reader
.read_until(b'\n', &mut bs)
.map_err(|err| PyIOError::new_err(err.to_string()))?;
bs.truncate(n);
bs
}
};

Buffer::new(buffer).into_bytes_ref(py)
}

/// Read bytes into a pre-allocated, writable buffer
pub fn readinto(&mut self, buffer: PyBuffer<u8>) -> PyResult<usize> {
let reader = match &mut self.0 {
Expand Down
30 changes: 29 additions & 1 deletion bindings/python/tests/test_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
# under the License.

import os
from random import randint
import io
from random import randint, choices
from uuid import uuid4

import pytest
Expand Down Expand Up @@ -73,6 +74,33 @@ def test_sync_reader(service_name, operator, async_operator):
operator.delete(filename)


@pytest.mark.need_capability("read", "write", "delete")
def test_sync_reader_readline(service_name, operator, async_operator):
size = randint(1, 1024)
lines = randint(1, min(100, size))
filename = f"random_file_{str(uuid4())}"
content = bytearray(os.urandom(size))

for idx in choices(range(0, size), k=lines):
content[idx] = ord("\n")
operator.write(filename, content)

line_contents = io.BytesIO(content).readlines()
i = 0

with operator.open(filename, "rb") as reader:
assert reader.readable()
assert not reader.writable()
assert not reader.closed

while (read_content := reader.readline()) != b"":
assert read_content is not None
assert read_content == line_contents[i]
i += 1

operator.delete(filename)


@pytest.mark.asyncio
@pytest.mark.need_capability("read", "write", "delete")
async def test_async_read(service_name, operator, async_operator):
Expand Down

0 comments on commit aa94e83

Please sign in to comment.