1use std::borrow::Cow;
2#[cfg(target_family = "unix")]
3use std::fs;
4use std::fs::File;
5use std::io;
6use std::io::{Cursor, ErrorKind, Read, Seek, SeekFrom, Write};
7#[cfg(target_family = "unix")]
8use std::os::fd::{FromRawFd, RawFd};
9use std::path::PathBuf;
10
11use polars::io::mmap::MmapBytesReader;
12use polars::prelude::PlRefPath;
13use polars::prelude::file::{Writable, WritableTrait};
14use polars_buffer::{Buffer, SharedStorage};
15use polars_error::polars_err;
16use pyo3::IntoPyObjectExt;
17use pyo3::exceptions::PyTypeError;
18use pyo3::prelude::*;
19use pyo3::types::{PyBytes, PyString, PyStringMethods};
20
21use crate::error::PyPolarsErr;
22use crate::prelude::resolve_homedir;
23use crate::utils::to_py_err;
24
25pub(crate) struct PyFileLikeObject {
26 inner: Py<PyAny>,
27 expects_str: bool,
29 has_flush: bool,
31}
32
33impl WritableTrait for PyFileLikeObject {
34 fn close(&mut self) -> io::Result<()> {
35 Ok(())
36 }
37
38 fn sync_all(&self) -> std::io::Result<()> {
39 self.flush()
40 }
41
42 fn sync_data(&self) -> std::io::Result<()> {
43 self.flush()
44 }
45}
46
47impl Clone for PyFileLikeObject {
48 fn clone(&self) -> Self {
49 Python::attach(|py| Self {
50 inner: self.inner.clone_ref(py),
51 expects_str: self.expects_str,
52 has_flush: self.has_flush,
53 })
54 }
55}
56
57impl PyFileLikeObject {
59 pub(crate) fn new(object: Py<PyAny>, expects_str: bool, has_flush: bool) -> Self {
63 PyFileLikeObject {
64 inner: object,
65 expects_str,
66 has_flush,
67 }
68 }
69
70 pub(crate) fn to_buffer(&self) -> Buffer<u8> {
71 Python::attach(|py| {
72 let bytes = self
73 .inner
74 .call_method(py, "read", (), None)
75 .expect("no read method found");
76
77 if let Ok(b) = bytes.cast_bound::<PyBytes>(py) {
78 let slice = b.as_bytes();
80 let owner = bytes.clone_ref(py);
81 let ss = unsafe { SharedStorage::from_slice_with_owner(slice, owner) };
82 return Buffer::from_storage(ss);
83 }
84
85 if let Ok(b) = bytes.cast_bound::<PyString>(py) {
86 return match b.to_cow().expect("PyString is not valid UTF-8") {
87 Cow::Borrowed(v) => {
88 let slice = v.as_bytes();
90 let owner = bytes.clone_ref(py);
91 let ss = unsafe { SharedStorage::from_slice_with_owner(slice, owner) };
92 return Buffer::from_storage(ss);
93 },
94 Cow::Owned(v) => Buffer::from_vec(v.into_bytes()),
95 };
96 }
97
98 panic!("Expecting to be able to downcast into bytes from read result.");
99 })
100 }
101
102 pub(crate) fn ensure_requirements(
106 object: &Bound<PyAny>,
107 read: bool,
108 write: bool,
109 seek: bool,
110 ) -> PyResult<()> {
111 if read && object.getattr("read").is_err() {
112 return Err(PyErr::new::<PyTypeError, _>(
113 "Object does not have a .read() method.",
114 ));
115 }
116
117 if seek && object.getattr("seek").is_err() {
118 return Err(PyErr::new::<PyTypeError, _>(
119 "Object does not have a .seek() method.",
120 ));
121 }
122
123 if write && object.getattr("write").is_err() {
124 return Err(PyErr::new::<PyTypeError, _>(
125 "Object does not have a .write() method.",
126 ));
127 }
128
129 Ok(())
130 }
131
132 pub fn flush(&self) -> std::io::Result<()> {
133 if self.has_flush {
134 Python::attach(|py| {
135 self.inner
136 .call_method(py, "flush", (), None)
137 .map_err(pyerr_to_io_err)
138 })?;
139 }
140
141 Ok(())
142 }
143}
144
145fn pyerr_to_io_err(e: PyErr) -> io::Error {
147 Python::attach(|py| {
148 let e_as_object: Py<PyAny> = e.into_py_any(py).unwrap();
149
150 match e_as_object.call_method(py, "__str__", (), None) {
151 Ok(repr) => match repr.extract::<String>(py) {
152 Ok(s) => io::Error::other(s),
153 Err(_e) => io::Error::other("An unknown error has occurred"),
154 },
155 Err(_) => io::Error::other("Err doesn't have __str__"),
156 }
157 })
158}
159
160impl Read for PyFileLikeObject {
161 fn read(&mut self, mut buf: &mut [u8]) -> Result<usize, io::Error> {
162 Python::attach(|py| {
163 let bytes = self
164 .inner
165 .call_method(py, "read", (buf.len(),), None)
166 .map_err(pyerr_to_io_err)?;
167
168 let opt_bytes = bytes.cast_bound::<PyBytes>(py);
169
170 if let Ok(bytes) = opt_bytes {
171 buf.write_all(bytes.as_bytes())?;
172
173 bytes.len().map_err(pyerr_to_io_err)
174 } else if let Ok(s) = bytes.cast_bound::<PyString>(py) {
175 let s = s.to_cow().map_err(pyerr_to_io_err)?;
176 buf.write_all(s.as_bytes())?;
177 Ok(s.len())
178 } else {
179 Err(io::Error::new(
180 ErrorKind::InvalidInput,
181 polars_err!(InvalidOperation: "could not read from input"),
182 ))
183 }
184 })
185 }
186}
187
188impl Write for PyFileLikeObject {
189 fn write(&mut self, buf: &[u8]) -> Result<usize, io::Error> {
190 let expects_str = self.expects_str;
195 let expects_str_and_is_ascii = expects_str && buf.is_ascii();
196
197 Python::attach(|py| {
198 let n_bytes = if expects_str_and_is_ascii {
199 let number_chars_written = unsafe {
200 self.inner.call_method(
201 py,
202 "write",
203 (PyString::new(py, std::str::from_utf8_unchecked(buf)),),
204 None,
205 )
206 }
207 .map_err(pyerr_to_io_err)?;
208 number_chars_written.extract(py).map_err(pyerr_to_io_err)?
209 } else if expects_str {
210 let number_chars_written = self
211 .inner
212 .call_method(
213 py,
214 "write",
215 (PyString::new(
216 py,
217 std::str::from_utf8(buf).map_err(io::Error::other)?,
218 ),),
219 None,
220 )
221 .map_err(pyerr_to_io_err)?;
222 let n_chars: usize = number_chars_written.extract(py).map_err(pyerr_to_io_err)?;
223 if n_chars > 0 {
225 std::str::from_utf8(buf)
226 .map(|str| {
227 str.char_indices()
228 .nth(n_chars - 1)
229 .map(|(i, ch)| i + ch.len_utf8())
230 .unwrap()
231 })
232 .expect("unable to parse buffer as utf-8")
233 } else {
234 0
235 }
236 } else {
237 let number_bytes_written = self
238 .inner
239 .call_method(py, "write", (PyBytes::new(py, buf),), None)
240 .map_err(pyerr_to_io_err)?;
241 number_bytes_written.extract(py).map_err(pyerr_to_io_err)?
242 };
243 Ok(n_bytes)
244 })
245 }
246
247 fn flush(&mut self) -> Result<(), io::Error> {
248 Self::flush(self)
249 }
250}
251
252impl Seek for PyFileLikeObject {
253 fn seek(&mut self, pos: SeekFrom) -> Result<u64, io::Error> {
254 Python::attach(|py| {
255 let (whence, offset) = match pos {
256 SeekFrom::Start(i) => (0, i as i64),
257 SeekFrom::Current(i) => (1, i),
258 SeekFrom::End(i) => (2, i),
259 };
260
261 let new_position = self
262 .inner
263 .call_method(py, "seek", (offset, whence), None)
264 .map_err(pyerr_to_io_err)?;
265
266 new_position.extract(py).map_err(pyerr_to_io_err)
267 })
268 }
269}
270
271pub(crate) trait FileLike: Read + Write + Seek + Sync + Send {}
272
273impl FileLike for File {}
274impl FileLike for PyFileLikeObject {}
275impl MmapBytesReader for PyFileLikeObject {}
276
277pub(crate) enum EitherRustPythonFile {
278 Py(PyFileLikeObject),
279 Rust(std::fs::File),
280}
281
282impl EitherRustPythonFile {
283 pub(crate) fn into_dyn(self) -> Box<dyn FileLike> {
284 match self {
285 EitherRustPythonFile::Py(f) => Box::new(f),
286 EitherRustPythonFile::Rust(f) => Box::new(f),
287 }
288 }
289
290 fn into_scan_source_input(self) -> PythonScanSourceInput {
291 match self {
292 EitherRustPythonFile::Py(f) => PythonScanSourceInput::Buffer(f.to_buffer()),
293 EitherRustPythonFile::Rust(f) => PythonScanSourceInput::File(f),
294 }
295 }
296
297 pub(crate) fn into_writable(self) -> Writable {
298 match self {
299 Self::Py(f) => Writable::Dyn(Box::new(f)),
300 Self::Rust(f) => Writable::Local(f),
301 }
302 }
303}
304
305pub(crate) enum PythonScanSourceInput {
306 Buffer(Buffer<u8>),
307 Path(PlRefPath),
308 File(std::fs::File),
309}
310
311pub(crate) fn try_get_pyfile(
312 py: Python<'_>,
313 py_f: Bound<'_, PyAny>,
314 write: bool,
315) -> PyResult<(EitherRustPythonFile, Option<PathBuf>)> {
316 let io = py.import("io")?;
317 let is_utf8_encoding = |py_f: &Bound<PyAny>| -> PyResult<bool> {
318 let encoding = py_f.getattr("encoding")?;
319 let encoding = encoding.extract::<Cow<str>>()?;
320 Ok(encoding.eq_ignore_ascii_case("utf-8") || encoding.eq_ignore_ascii_case("utf8"))
321 };
322
323 #[cfg(target_family = "unix")]
324 if let Some(fd) = (py_f.is_exact_instance(&io.getattr("FileIO").unwrap())
325 || (py_f.is_exact_instance(&io.getattr("BufferedReader").unwrap())
326 || py_f.is_exact_instance(&io.getattr("BufferedWriter").unwrap())
327 || py_f.is_exact_instance(&io.getattr("BufferedRandom").unwrap())
328 || py_f.is_exact_instance(&io.getattr("BufferedRWPair").unwrap())
329 || (py_f.is_exact_instance(&io.getattr("TextIOWrapper").unwrap())
330 && is_utf8_encoding(&py_f)?))
331 && if write {
332 py_f.call_method0("flush").is_ok()
334 } else {
335 py_f.call_method1("seek", (0, 1)).is_ok()
337 })
338 .then(|| {
339 py_f.getattr("fileno")
340 .and_then(|fileno| fileno.call0())
341 .and_then(|fileno| fileno.extract::<libc::c_int>())
342 .ok()
343 })
344 .flatten()
345 .map(|fileno| unsafe {
346 libc::fcntl(fileno, libc::F_DUPFD_CLOEXEC, 0)
363 })
364 .filter(|fileno| *fileno != -1)
365 .map(|fileno| fileno as RawFd)
366 {
367 return Ok((
368 EitherRustPythonFile::Rust(unsafe { File::from_raw_fd(fd).into() }),
369 fs::canonicalize(format!("/proc/self/fd/{fd}")).ok(),
372 ));
373 }
374
375 let py_f = if py_f
378 .is_instance(&io.getattr("TextIOWrapper").unwrap())
379 .unwrap_or_default()
380 {
381 if !is_utf8_encoding(&py_f)? {
382 return Err(PyPolarsErr::from(
383 polars_err!(InvalidOperation: "file encoding is not UTF-8"),
384 )
385 .into());
386 }
387 if write {
390 py_f.call_method0("flush")?;
391 } else {
392 py_f.call_method1("seek", (0, 1))?;
393 }
394 py_f.getattr("buffer")?
395 } else {
396 py_f
397 };
398 PyFileLikeObject::ensure_requirements(&py_f, !write, write, !write)?;
399 let expects_str = py_f.is_instance(&io.getattr("TextIOBase").unwrap())?;
400 let has_flush = py_f
401 .getattr_opt("flush")?
402 .is_some_and(|flush| flush.is_callable());
403 let f = PyFileLikeObject::new(py_f.unbind(), expects_str, has_flush);
404 Ok((EitherRustPythonFile::Py(f), None))
405}
406
407pub(crate) fn get_python_scan_source_input(
408 py_f: Py<PyAny>,
409 write: bool,
410) -> PyResult<PythonScanSourceInput> {
411 Python::attach(|py| {
412 let py_f = py_f.into_bound(py);
413
414 let py_f = read_if_bytesio(py_f);
419
420 if let Ok(b) = py_f.cast::<PyBytes>() {
422 let slice = b.as_bytes();
424 let owner = b.clone().unbind();
425 let ss = unsafe { SharedStorage::from_slice_with_owner(slice, owner) };
426 let buffer = Buffer::from_storage(ss);
427 return Ok(PythonScanSourceInput::Buffer(buffer));
428 }
429
430 if let Ok(s) = py_f.extract::<Cow<str>>() {
431 let file_path = PlRefPath::try_from_path(resolve_homedir(s.as_ref()).as_ref())
432 .map_err(to_py_err)?;
433
434 Ok(PythonScanSourceInput::Path(file_path))
435 } else {
436 Ok(try_get_pyfile(py, py_f, write)?.0.into_scan_source_input())
437 }
438 })
439}
440
441fn get_either_buffer_or_path(
442 py_f: Py<PyAny>,
443 write: bool,
444) -> PyResult<(EitherRustPythonFile, Option<PathBuf>)> {
445 Python::attach(|py| {
446 let py_f = py_f.into_bound(py);
447 if let Ok(s) = py_f.extract::<Cow<str>>() {
448 let file_path = resolve_homedir(s.as_ref());
449 let f = if write {
450 polars_utils::io::create_file(&file_path).map_err(PyPolarsErr::from)?
451 } else {
452 polars_utils::io::open_file(&file_path).map_err(PyPolarsErr::from)?
453 };
454 Ok((
455 EitherRustPythonFile::Rust(f.into()),
456 Some(file_path.into_owned()),
457 ))
458 } else {
459 try_get_pyfile(py, py_f, write)
460 }
461 })
462}
463
464pub(crate) fn get_either_file(py_f: Py<PyAny>, write: bool) -> PyResult<EitherRustPythonFile> {
468 Ok(get_either_buffer_or_path(py_f, write)?.0)
469}
470
471pub(crate) fn get_file_like(f: Py<PyAny>, truncate: bool) -> PyResult<Box<dyn FileLike>> {
472 Ok(get_either_file(f, truncate)?.into_dyn())
473}
474
475fn read_if_bytesio(py_f: Bound<PyAny>) -> Bound<PyAny> {
478 let bytes_io = py_f.py().import("io").unwrap().getattr("BytesIO").unwrap();
479 if py_f.is_instance(&bytes_io).unwrap() {
480 let Ok(bytes) = py_f.call_method0("getvalue") else {
483 return py_f;
484 };
485 return bytes;
486 }
487 py_f
488}
489
490pub(crate) fn get_mmap_bytes_reader(py_f: &Bound<PyAny>) -> PyResult<Box<dyn MmapBytesReader>> {
492 get_mmap_bytes_reader_and_path(py_f).map(|t| t.0)
493}
494
495pub(crate) fn get_mmap_bytes_reader_and_path(
496 py_f: &Bound<PyAny>,
497) -> PyResult<(Box<dyn MmapBytesReader>, Option<PathBuf>)> {
498 let py_f = read_if_bytesio(py_f.clone());
499
500 if let Ok(bytes) = py_f.cast::<PyBytes>() {
502 let slice = bytes.as_bytes();
504 let owner = bytes.clone().unbind();
505 let ss = unsafe { SharedStorage::from_slice_with_owner(slice, owner) };
506 Ok((Box::new(Cursor::new(Buffer::from_storage(ss))), None))
507 }
508 else {
510 match get_either_buffer_or_path(py_f.to_owned().unbind(), false)? {
511 (EitherRustPythonFile::Rust(f), path) => Ok((Box::new(f), path)),
512 (EitherRustPythonFile::Py(f), path) => Ok((Box::new(Cursor::new(f.to_buffer())), path)),
513 }
514 }
515}