pyo3-file-0.7.0/.cargo_vcs_info.json0000644000000001360000000000100126470ustar { "git": { "sha1": "258a87f4956b4c2cfbff730c208d5575b1db448e" }, "path_in_vcs": "" }pyo3-file-0.7.0/.github/workflows/ci.yml000064400000000000000000000017041046102023000161540ustar 00000000000000name: ci on: push: branches: - "*" jobs: test: name: test runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v2 with: fetch-depth: 1 - name: Install dependencies run: | python -m pip install -U pip pip install -U setuptools wheel pytest - name: Install Rust uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.rust }} profile: minimal override: true target: ${{ matrix.target }} - name: Build wheel uses: messense/maturin-action@v1 with: manylinux: auto command: build args: -m ./examples/path_or_file_like/Cargo.toml -o wheels - name: Install wheel run: | pip install ./wheels/*.whl - name: Test run: | pytest working-directory: ./examples/path_or_file_like pyo3-file-0.7.0/.github/workflows/release.yml000064400000000000000000000007701046102023000172030ustar 00000000000000name: release on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" jobs: create-release: name: Release crate runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: fetch-depth: 1 - name: Setup Rust toolchain uses: actions-rs/toolchain@v1 with: toolchain: stable - name: Publish crate uses: actions-rs/cargo@v1 with: command: publish args: --token ${{ secrets.CRATES_IO_TOKEN }} pyo3-file-0.7.0/.gitignore000064400000000000000000000001221046102023000134220ustar 00000000000000**/target **/*.egg-info **/*.rs.bk **/*.so *.pyc .idea .history Cargo.lock .venv pyo3-file-0.7.0/.vscode/settings.json000064400000000000000000000001661046102023000155360ustar 00000000000000{ "rust-analyzer.linkedProjects": [ "Cargo.toml", "examples/path_or_file_like/Cargo.toml" ] } pyo3-file-0.7.0/CHANGELOG.md000064400000000000000000000013741046102023000132550ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.5.0] - 2022-04-16 - Update to PyO3 0.16 - Add textio support (thanks @ethanhs) - Remove `Cargo.lock` (as this is a library) ## [0.4.0] - 2021-07-09 Update to PyO3 0.14 ## [0.3.0] - 2019-10-30 Update to PyO3 0.8.2 ## [0.2.0] - 2019-05-20 Includes a minor breaking change to constructor behavior. ### Changed - Added another constructor `PyFileLikeObject::require` that validates the object has the required method, `PyFileLikeObject::new` now cannot fail. ## [0.1.0] - 2019-05-20 Initial release pyo3-file-0.7.0/Cargo.toml0000644000000017240000000000100106510ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "pyo3-file" version = "0.7.0" authors = ["Omer Ben-Amram "] description = "A small helper library for working with python file-like objects with rust" homepage = "https://github.com/omerbenamram/pyo3-file" readme = "README.md" license = "MIT/Apache-2.0" repository = "https://github.com/omerbenamram/pyo3-file" [dependencies.pyo3] version = ">=0.18.1,<0.20" [dev-dependencies.skeptic] version = "0.13.7" [build-dependencies.skeptic] version = "0.13.7" pyo3-file-0.7.0/Cargo.toml.orig000064400000000000000000000007701046102023000143320ustar 00000000000000[package] name = "pyo3-file" description = "A small helper library for working with python file-like objects with rust" homepage = "https://github.com/omerbenamram/pyo3-file" repository = "https://github.com/omerbenamram/pyo3-file" license = "MIT/Apache-2.0" readme = "README.md" version = "0.7.0" authors = ["Omer Ben-Amram "] edition = "2018" [dependencies] pyo3 = { version = ">=0.18.1,<0.20" } [dev-dependencies] skeptic = "0.13.7" [build-dependencies] skeptic = "0.13.7" pyo3-file-0.7.0/README.md000064400000000000000000000053751046102023000127300ustar 00000000000000![crates.io](https://img.shields.io/crates/v/pyo3-file.svg) # PyO3-File This is a small utility library to facilitate working with python file-like objects with rust. ## Example An example use case for this is when a file is opened in python, and needs to be passed to a rust library. We could support both by introspecting the `PyObject`, and pick the correct behavior. We would like this to work: ```python from path_or_file_like import accepts_path_or_file_like def main(): # should open `some_file.txt`. accepts_path_or_file_like("./some_file.txt") # should read from the python handle. f = open('./some_file.txt') accepts_path_or_file_like(f) ``` We could use `pyo3_file` to extend an existing a `pyo3` module. ```rust use pyo3_file::PyFileLikeObject; use pyo3::types::PyString; use pyo3::prelude::*; use pyo3::wrap_pyfunction; use std::io::Read; use std::fs::File; /// Represents either a path `File` or a file-like object `FileLike` #[derive(Debug)] enum FileOrFileLike { File(String), FileLike(PyFileLikeObject), } impl FileOrFileLike { pub fn from_pyobject(path_or_file_like: PyObject) -> PyResult { Python::with_gil(|py| { // is a path if let Ok(string_ref) = path_or_file_like.cast_as::(py) { return Ok(FileOrFileLike::File( string_ref.to_string_lossy().to_string(), )); } // is a file-like match PyFileLikeObject::with_requirements(path_or_file_like, true, false, true) { Ok(f) => Ok(FileOrFileLike::FileLike(f)), Err(e) => Err(e) } }) } } #[pyfunction] /// Opens a file or file-like, and reads it to string. fn accepts_path_or_file_like( path_or_file_like: PyObject, ) -> PyResult { Python::with_gil(|py| { match FileOrFileLike::from_pyobject(path_or_file_like) { Ok(f) => match f { FileOrFileLike::File(s) => { println!("It's a file! - path {}", s); let mut f = File::open(s)?; let mut string = String::new(); let read = f.read_to_string(&mut string); Ok(string) } FileOrFileLike::FileLike(mut f) => { println!("Its a file-like object"); let mut string = String::new(); let read = f.read_to_string(&mut string); Ok(string) } }, Err(e) => Err(e), } }) } #[pymodule] fn path_or_file_like(_py: Python, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(accepts_path_or_file_like))?; Ok(()) } # fn main() {} ``` pyo3-file-0.7.0/build.rs000064400000000000000000000000771046102023000131100ustar 00000000000000fn main() { skeptic::generate_doc_tests(&["README.md"]); } pyo3-file-0.7.0/rust-toolchain000064400000000000000000000000071046102023000143320ustar 00000000000000stable pyo3-file-0.7.0/src/lib.rs000064400000000000000000000134531046102023000133500ustar 00000000000000use pyo3::{exceptions::PyTypeError, prelude::*}; use pyo3::types::{PyBytes, PyString, PyType}; use std::io; use std::io::{Read, Seek, SeekFrom, Write}; #[derive(Debug)] pub struct PyFileLikeObject { inner: PyObject, is_text_io: bool, } /// Wraps a `PyObject`, and implements read, seek, and write for it. impl PyFileLikeObject { /// Creates an instance of a `PyFileLikeObject` from a `PyObject`. /// To assert the object has the required methods methods, /// instantiate it with `PyFileLikeObject::require` pub fn new(object: PyObject) -> PyResult { Python::with_gil(|py| { let io = PyModule::import(py, "io")?; let text_io = io.getattr("TextIOBase")?; let text_io_type = text_io.extract::<&PyType>()?; let is_text_io = object.as_ref(py).is_instance(text_io_type)?; Ok(PyFileLikeObject { inner: object, is_text_io, }) }) } /// Same as `PyFileLikeObject::new`, but validates that the underlying /// python object has a `read`, `write`, and `seek` methods in respect to parameters. /// Will return a `TypeError` if object does not have `read`, `seek`, and `write` methods. pub fn with_requirements( object: PyObject, read: bool, write: bool, seek: bool, ) -> PyResult { Python::with_gil(|py| { if read && object.getattr(py, "read").is_err() { return Err(PyErr::new::( "Object does not have a .read() method.", )); } if seek && object.getattr(py, "seek").is_err() { return Err(PyErr::new::( "Object does not have a .seek() method.", )); } if write && object.getattr(py, "write").is_err() { return Err(PyErr::new::( "Object does not have a .write() method.", )); } PyFileLikeObject::new(object) }) } } /// Extracts a string repr from, and returns an IO error to send back to rust. fn pyerr_to_io_err(e: PyErr) -> io::Error { Python::with_gil(|py| { let e_as_object: PyObject = e.into_py(py); match e_as_object.call_method(py, "__str__", (), None) { Ok(repr) => match repr.extract::(py) { Ok(s) => io::Error::new(io::ErrorKind::Other, s), Err(_e) => io::Error::new(io::ErrorKind::Other, "An unknown error has occurred"), }, Err(_) => io::Error::new(io::ErrorKind::Other, "Err doesn't have __str__"), } }) } impl Read for PyFileLikeObject { fn read(&mut self, mut buf: &mut [u8]) -> Result { Python::with_gil(|py| { if self.is_text_io { if buf.len() < 4 { return Err(io::Error::new( io::ErrorKind::Other, "buffer size must be at least 4 bytes", )); } let res = self .inner .call_method(py, "read", (buf.len() / 4,), None) .map_err(pyerr_to_io_err)?; let pystring: &PyString = res .downcast(py) .expect("Expecting to be able to downcast into str from read result."); let bytes = pystring.to_str().unwrap().as_bytes(); buf.write_all(bytes)?; Ok(bytes.len()) } else { let res = self .inner .call_method(py, "read", (buf.len(),), None) .map_err(pyerr_to_io_err)?; let pybytes: &PyBytes = res .downcast(py) .expect("Expecting to be able to downcast into bytes from read result."); let bytes = pybytes.as_bytes(); buf.write_all(bytes)?; Ok(bytes.len()) } }) } } impl Write for PyFileLikeObject { fn write(&mut self, buf: &[u8]) -> Result { Python::with_gil(|py| { let arg = if self.is_text_io { let s = std::str::from_utf8(buf) .expect("Tried to write non-utf8 data to a TextIO object."); PyString::new(py, s).to_object(py) } else { PyBytes::new(py, buf).to_object(py) }; let number_bytes_written = self .inner .call_method(py, "write", (arg,), None) .map_err(pyerr_to_io_err)?; if number_bytes_written.is_none(py) { return Err(io::Error::new( io::ErrorKind::Other, "write() returned None, expected number of bytes written", )); } number_bytes_written.extract(py).map_err(pyerr_to_io_err) }) } fn flush(&mut self) -> Result<(), io::Error> { Python::with_gil(|py| { self.inner .call_method(py, "flush", (), None) .map_err(pyerr_to_io_err)?; Ok(()) }) } } impl Seek for PyFileLikeObject { fn seek(&mut self, pos: SeekFrom) -> Result { Python::with_gil(|py| { let (whence, offset) = match pos { SeekFrom::Start(i) => (0, i as i64), SeekFrom::Current(i) => (1, i), SeekFrom::End(i) => (2, i), }; let new_position = self .inner .call_method(py, "seek", (offset, whence), None) .map_err(pyerr_to_io_err)?; new_position.extract(py).map_err(pyerr_to_io_err) }) } } pyo3-file-0.7.0/tests/skeptic.rs000064400000000000000000000000701046102023000146060ustar 00000000000000include!(concat!(env!("OUT_DIR"), "/skeptic-tests.rs"));