polars_python/conversion/
mod.rs

1pub(crate) mod any_value;
2pub(crate) mod chunked_array;
3mod datetime;
4
5use std::convert::Infallible;
6use std::fmt::{Display, Formatter};
7use std::fs::File;
8use std::hash::{Hash, Hasher};
9use std::path::PathBuf;
10
11#[cfg(feature = "object")]
12use polars::chunked_array::object::PolarsObjectSafe;
13use polars::frame::row::Row;
14#[cfg(feature = "avro")]
15use polars::io::avro::AvroCompression;
16#[cfg(feature = "cloud")]
17use polars::io::cloud::CloudOptions;
18use polars::series::ops::NullBehavior;
19use polars_core::utils::arrow::array::Array;
20use polars_core::utils::arrow::types::NativeType;
21use polars_core::utils::materialize_dyn_int;
22use polars_lazy::prelude::*;
23#[cfg(feature = "parquet")]
24use polars_parquet::write::StatisticsOptions;
25use polars_plan::dsl::ScanSources;
26use polars_utils::mmap::MemSlice;
27use polars_utils::pl_str::PlSmallStr;
28use polars_utils::total_ord::{TotalEq, TotalHash};
29use pyo3::basic::CompareOp;
30use pyo3::exceptions::{PyTypeError, PyValueError};
31use pyo3::intern;
32use pyo3::prelude::*;
33use pyo3::pybacked::PyBackedStr;
34use pyo3::sync::GILOnceCell;
35use pyo3::types::{PyDict, PyList, PySequence, PyString};
36
37use crate::error::PyPolarsErr;
38use crate::expr::PyExpr;
39use crate::file::{PythonScanSourceInput, get_python_scan_source_input};
40#[cfg(feature = "object")]
41use crate::object::OBJECT_NAME;
42use crate::prelude::*;
43use crate::py_modules::{pl_series, polars};
44use crate::series::PySeries;
45use crate::utils::to_py_err;
46use crate::{PyDataFrame, PyLazyFrame};
47
48/// # Safety
49/// Should only be implemented for transparent types
50pub(crate) unsafe trait Transparent {
51    type Target;
52}
53
54unsafe impl Transparent for PySeries {
55    type Target = Series;
56}
57
58unsafe impl<T> Transparent for Wrap<T> {
59    type Target = T;
60}
61
62unsafe impl<T: Transparent> Transparent for Option<T> {
63    type Target = Option<T::Target>;
64}
65
66pub(crate) fn reinterpret_vec<T: Transparent>(input: Vec<T>) -> Vec<T::Target> {
67    assert_eq!(size_of::<T>(), size_of::<T::Target>());
68    assert_eq!(align_of::<T>(), align_of::<T::Target>());
69    let len = input.len();
70    let cap = input.capacity();
71    let mut manual_drop_vec = std::mem::ManuallyDrop::new(input);
72    let vec_ptr: *mut T = manual_drop_vec.as_mut_ptr();
73    let ptr: *mut T::Target = vec_ptr as *mut T::Target;
74    unsafe { Vec::from_raw_parts(ptr, len, cap) }
75}
76
77pub(crate) fn vec_extract_wrapped<T>(buf: Vec<Wrap<T>>) -> Vec<T> {
78    reinterpret_vec(buf)
79}
80
81#[derive(PartialEq, Eq, Hash)]
82#[repr(transparent)]
83pub struct Wrap<T>(pub T);
84
85impl<T> Clone for Wrap<T>
86where
87    T: Clone,
88{
89    fn clone(&self) -> Self {
90        Wrap(self.0.clone())
91    }
92}
93impl<T> From<T> for Wrap<T> {
94    fn from(t: T) -> Self {
95        Wrap(t)
96    }
97}
98
99// extract a Rust DataFrame from a python DataFrame, that is DataFrame<PyDataFrame<RustDataFrame>>
100pub(crate) fn get_df(obj: &Bound<'_, PyAny>) -> PyResult<DataFrame> {
101    let pydf = obj.getattr(intern!(obj.py(), "_df"))?;
102    Ok(pydf.extract::<PyDataFrame>()?.df)
103}
104
105pub(crate) fn get_lf(obj: &Bound<'_, PyAny>) -> PyResult<LazyFrame> {
106    let pydf = obj.getattr(intern!(obj.py(), "_ldf"))?;
107    Ok(pydf.extract::<PyLazyFrame>()?.ldf)
108}
109
110pub(crate) fn get_series(obj: &Bound<'_, PyAny>) -> PyResult<Series> {
111    let s = obj.getattr(intern!(obj.py(), "_s"))?;
112    Ok(s.extract::<PySeries>()?.series)
113}
114
115pub(crate) fn to_series(py: Python<'_>, s: PySeries) -> PyResult<Bound<PyAny>> {
116    let series = pl_series(py).bind(py);
117    let constructor = series.getattr(intern!(py, "_from_pyseries"))?;
118    constructor.call1((s,))
119}
120
121impl<'py> FromPyObject<'py> for Wrap<PlSmallStr> {
122    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
123        Ok(Wrap((&*ob.extract::<PyBackedStr>()?).into()))
124    }
125}
126
127#[cfg(feature = "csv")]
128impl<'py> FromPyObject<'py> for Wrap<NullValues> {
129    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
130        if let Ok(s) = ob.extract::<PyBackedStr>() {
131            Ok(Wrap(NullValues::AllColumnsSingle((&*s).into())))
132        } else if let Ok(s) = ob.extract::<Vec<PyBackedStr>>() {
133            Ok(Wrap(NullValues::AllColumns(
134                s.into_iter().map(|x| (&*x).into()).collect(),
135            )))
136        } else if let Ok(s) = ob.extract::<Vec<(PyBackedStr, PyBackedStr)>>() {
137            Ok(Wrap(NullValues::Named(
138                s.into_iter()
139                    .map(|(a, b)| ((&*a).into(), (&*b).into()))
140                    .collect(),
141            )))
142        } else {
143            Err(
144                PyPolarsErr::Other("could not extract value from null_values argument".into())
145                    .into(),
146            )
147        }
148    }
149}
150
151fn struct_dict<'a, 'py>(
152    py: Python<'py>,
153    vals: impl Iterator<Item = AnyValue<'a>>,
154    flds: &[Field],
155) -> PyResult<Bound<'py, PyDict>> {
156    let dict = PyDict::new(py);
157    flds.iter().zip(vals).try_for_each(|(fld, val)| {
158        dict.set_item(fld.name().as_str(), Wrap(val).into_pyobject(py)?)
159    })?;
160    Ok(dict)
161}
162
163// accept u128 array to ensure alignment is correct
164fn decimal_to_digits(v: i128, buf: &mut [u128; 3]) -> usize {
165    const ZEROS: i128 = 0x3030_3030_3030_3030_3030_3030_3030_3030;
166    // SAFETY: transmute is safe as there are 48 bytes in 3 128bit ints
167    // and the minimal alignment of u8 fits u16
168    let buf = unsafe { std::mem::transmute::<&mut [u128; 3], &mut [u8; 48]>(buf) };
169    let mut buffer = itoa::Buffer::new();
170    let value = buffer.format(v);
171    let len = value.len();
172    for (dst, src) in buf.iter_mut().zip(value.as_bytes().iter()) {
173        *dst = *src
174    }
175
176    let ptr = buf.as_mut_ptr() as *mut i128;
177    unsafe {
178        // this is safe because we know that the buffer is exactly 48 bytes long
179        *ptr -= ZEROS;
180        *ptr.add(1) -= ZEROS;
181        *ptr.add(2) -= ZEROS;
182    }
183    len
184}
185
186impl<'py> IntoPyObject<'py> for &Wrap<DataType> {
187    type Target = PyAny;
188    type Output = Bound<'py, Self::Target>;
189    type Error = PyErr;
190
191    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
192        let pl = polars(py).bind(py);
193
194        match &self.0 {
195            DataType::Int8 => {
196                let class = pl.getattr(intern!(py, "Int8"))?;
197                class.call0()
198            },
199            DataType::Int16 => {
200                let class = pl.getattr(intern!(py, "Int16"))?;
201                class.call0()
202            },
203            DataType::Int32 => {
204                let class = pl.getattr(intern!(py, "Int32"))?;
205                class.call0()
206            },
207            DataType::Int64 => {
208                let class = pl.getattr(intern!(py, "Int64"))?;
209                class.call0()
210            },
211            DataType::UInt8 => {
212                let class = pl.getattr(intern!(py, "UInt8"))?;
213                class.call0()
214            },
215            DataType::UInt16 => {
216                let class = pl.getattr(intern!(py, "UInt16"))?;
217                class.call0()
218            },
219            DataType::UInt32 => {
220                let class = pl.getattr(intern!(py, "UInt32"))?;
221                class.call0()
222            },
223            DataType::UInt64 => {
224                let class = pl.getattr(intern!(py, "UInt64"))?;
225                class.call0()
226            },
227            DataType::Int128 => {
228                let class = pl.getattr(intern!(py, "Int128"))?;
229                class.call0()
230            },
231            DataType::Float32 => {
232                let class = pl.getattr(intern!(py, "Float32"))?;
233                class.call0()
234            },
235            DataType::Float64 | DataType::Unknown(UnknownKind::Float) => {
236                let class = pl.getattr(intern!(py, "Float64"))?;
237                class.call0()
238            },
239            DataType::Decimal(precision, scale) => {
240                let class = pl.getattr(intern!(py, "Decimal"))?;
241                let args = (*precision, *scale);
242                class.call1(args)
243            },
244            DataType::Boolean => {
245                let class = pl.getattr(intern!(py, "Boolean"))?;
246                class.call0()
247            },
248            DataType::String | DataType::Unknown(UnknownKind::Str) => {
249                let class = pl.getattr(intern!(py, "String"))?;
250                class.call0()
251            },
252            DataType::Binary => {
253                let class = pl.getattr(intern!(py, "Binary"))?;
254                class.call0()
255            },
256            DataType::Array(inner, size) => {
257                let class = pl.getattr(intern!(py, "Array"))?;
258                let inner = Wrap(*inner.clone());
259                let args = (&inner, *size);
260                class.call1(args)
261            },
262            DataType::List(inner) => {
263                let class = pl.getattr(intern!(py, "List"))?;
264                let inner = Wrap(*inner.clone());
265                class.call1((&inner,))
266            },
267            DataType::Date => {
268                let class = pl.getattr(intern!(py, "Date"))?;
269                class.call0()
270            },
271            DataType::Datetime(tu, tz) => {
272                let datetime_class = pl.getattr(intern!(py, "Datetime"))?;
273                datetime_class.call1((tu.to_ascii(), tz.as_deref().map(|x| x.as_str())))
274            },
275            DataType::Duration(tu) => {
276                let duration_class = pl.getattr(intern!(py, "Duration"))?;
277                duration_class.call1((tu.to_ascii(),))
278            },
279            #[cfg(feature = "object")]
280            DataType::Object(_) => {
281                let class = pl.getattr(intern!(py, "Object"))?;
282                class.call0()
283            },
284            DataType::Categorical(_, ordering) => {
285                let class = pl.getattr(intern!(py, "Categorical"))?;
286                class.call1((Wrap(*ordering),))
287            },
288            DataType::Enum(rev_map, _) => {
289                // we should always have an initialized rev_map coming from rust
290                let categories = rev_map.as_ref().unwrap().get_categories();
291                let class = pl.getattr(intern!(py, "Enum"))?;
292                let s =
293                    Series::from_arrow(PlSmallStr::from_static("category"), categories.to_boxed())
294                        .map_err(PyPolarsErr::from)?;
295                let series = to_series(py, s.into())?;
296                class.call1((series,))
297            },
298            DataType::Time => pl.getattr(intern!(py, "Time")),
299            DataType::Struct(fields) => {
300                let field_class = pl.getattr(intern!(py, "Field"))?;
301                let iter = fields.iter().map(|fld| {
302                    let name = fld.name().as_str();
303                    let dtype = Wrap(fld.dtype().clone());
304                    field_class.call1((name, &dtype)).unwrap()
305                });
306                let fields = PyList::new(py, iter)?;
307                let struct_class = pl.getattr(intern!(py, "Struct"))?;
308                struct_class.call1((fields,))
309            },
310            DataType::Null => {
311                let class = pl.getattr(intern!(py, "Null"))?;
312                class.call0()
313            },
314            DataType::Unknown(UnknownKind::Int(v)) => {
315                Wrap(materialize_dyn_int(*v).dtype()).into_pyobject(py)
316            },
317            DataType::Unknown(_) => {
318                let class = pl.getattr(intern!(py, "Unknown"))?;
319                class.call0()
320            },
321            DataType::BinaryOffset => {
322                unimplemented!()
323            },
324        }
325    }
326}
327
328impl<'py> FromPyObject<'py> for Wrap<Field> {
329    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
330        let py = ob.py();
331        let name = ob
332            .getattr(intern!(py, "name"))?
333            .str()?
334            .extract::<PyBackedStr>()?;
335        let dtype = ob
336            .getattr(intern!(py, "dtype"))?
337            .extract::<Wrap<DataType>>()?;
338        Ok(Wrap(Field::new((&*name).into(), dtype.0)))
339    }
340}
341
342impl<'py> FromPyObject<'py> for Wrap<DataType> {
343    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
344        let py = ob.py();
345        let type_name = ob.get_type().qualname()?.to_string();
346
347        let dtype = match &*type_name {
348            "DataTypeClass" => {
349                // just the class, not an object
350                let name = ob
351                    .getattr(intern!(py, "__name__"))?
352                    .str()?
353                    .extract::<PyBackedStr>()?;
354                match &*name {
355                    "Int8" => DataType::Int8,
356                    "Int16" => DataType::Int16,
357                    "Int32" => DataType::Int32,
358                    "Int64" => DataType::Int64,
359                    "Int128" => DataType::Int128,
360                    "UInt8" => DataType::UInt8,
361                    "UInt16" => DataType::UInt16,
362                    "UInt32" => DataType::UInt32,
363                    "UInt64" => DataType::UInt64,
364                    "Float32" => DataType::Float32,
365                    "Float64" => DataType::Float64,
366                    "Boolean" => DataType::Boolean,
367                    "String" => DataType::String,
368                    "Binary" => DataType::Binary,
369                    "Categorical" => DataType::Categorical(None, Default::default()),
370                    "Enum" => DataType::Enum(None, Default::default()),
371                    "Date" => DataType::Date,
372                    "Time" => DataType::Time,
373                    "Datetime" => DataType::Datetime(TimeUnit::Microseconds, None),
374                    "Duration" => DataType::Duration(TimeUnit::Microseconds),
375                    "Decimal" => DataType::Decimal(None, None), // "none" scale => "infer"
376                    "List" => DataType::List(Box::new(DataType::Null)),
377                    "Array" => DataType::Array(Box::new(DataType::Null), 0),
378                    "Struct" => DataType::Struct(vec![]),
379                    "Null" => DataType::Null,
380                    #[cfg(feature = "object")]
381                    "Object" => DataType::Object(OBJECT_NAME),
382                    "Unknown" => DataType::Unknown(Default::default()),
383                    dt => {
384                        return Err(PyTypeError::new_err(format!(
385                            "'{dt}' is not a Polars data type",
386                        )));
387                    },
388                }
389            },
390            "Int8" => DataType::Int8,
391            "Int16" => DataType::Int16,
392            "Int32" => DataType::Int32,
393            "Int64" => DataType::Int64,
394            "Int128" => DataType::Int128,
395            "UInt8" => DataType::UInt8,
396            "UInt16" => DataType::UInt16,
397            "UInt32" => DataType::UInt32,
398            "UInt64" => DataType::UInt64,
399            "Float32" => DataType::Float32,
400            "Float64" => DataType::Float64,
401            "Boolean" => DataType::Boolean,
402            "String" => DataType::String,
403            "Binary" => DataType::Binary,
404            "Categorical" => {
405                let ordering = ob.getattr(intern!(py, "ordering")).unwrap();
406                let ordering = ordering.extract::<Wrap<CategoricalOrdering>>()?.0;
407                DataType::Categorical(None, ordering)
408            },
409            "Enum" => {
410                let categories = ob.getattr(intern!(py, "categories")).unwrap();
411                let s = get_series(&categories.as_borrowed())?;
412                let ca = s.str().map_err(PyPolarsErr::from)?;
413                let categories = ca.downcast_iter().next().unwrap().clone();
414                create_enum_dtype(categories)
415            },
416            "Date" => DataType::Date,
417            "Time" => DataType::Time,
418            "Datetime" => {
419                let time_unit = ob.getattr(intern!(py, "time_unit")).unwrap();
420                let time_unit = time_unit.extract::<Wrap<TimeUnit>>()?.0;
421                let time_zone = ob.getattr(intern!(py, "time_zone")).unwrap();
422                let time_zone = time_zone.extract::<Option<PyBackedStr>>()?;
423                DataType::Datetime(
424                    time_unit,
425                    TimeZone::opt_try_new(time_zone.as_deref()).map_err(to_py_err)?,
426                )
427            },
428            "Duration" => {
429                let time_unit = ob.getattr(intern!(py, "time_unit")).unwrap();
430                let time_unit = time_unit.extract::<Wrap<TimeUnit>>()?.0;
431                DataType::Duration(time_unit)
432            },
433            "Decimal" => {
434                let precision = ob.getattr(intern!(py, "precision"))?.extract()?;
435                let scale = ob.getattr(intern!(py, "scale"))?.extract()?;
436                DataType::Decimal(precision, Some(scale))
437            },
438            "List" => {
439                let inner = ob.getattr(intern!(py, "inner")).unwrap();
440                let inner = inner.extract::<Wrap<DataType>>()?;
441                DataType::List(Box::new(inner.0))
442            },
443            "Array" => {
444                let inner = ob.getattr(intern!(py, "inner")).unwrap();
445                let size = ob.getattr(intern!(py, "size")).unwrap();
446                let inner = inner.extract::<Wrap<DataType>>()?;
447                let size = size.extract::<usize>()?;
448                DataType::Array(Box::new(inner.0), size)
449            },
450            "Struct" => {
451                let fields = ob.getattr(intern!(py, "fields"))?;
452                let fields = fields
453                    .extract::<Vec<Wrap<Field>>>()?
454                    .into_iter()
455                    .map(|f| f.0)
456                    .collect::<Vec<Field>>();
457                DataType::Struct(fields)
458            },
459            "Null" => DataType::Null,
460            #[cfg(feature = "object")]
461            "Object" => DataType::Object(OBJECT_NAME),
462            "Unknown" => DataType::Unknown(Default::default()),
463            dt => {
464                return Err(PyTypeError::new_err(format!(
465                    "'{dt}' is not a Polars data type",
466                )));
467            },
468        };
469        Ok(Wrap(dtype))
470    }
471}
472
473impl<'py> IntoPyObject<'py> for Wrap<CategoricalOrdering> {
474    type Target = PyString;
475    type Output = Bound<'py, Self::Target>;
476    type Error = Infallible;
477
478    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
479        match self.0 {
480            CategoricalOrdering::Physical => "physical",
481            CategoricalOrdering::Lexical => "lexical",
482        }
483        .into_pyobject(py)
484    }
485}
486
487impl<'py> IntoPyObject<'py> for Wrap<TimeUnit> {
488    type Target = PyString;
489    type Output = Bound<'py, Self::Target>;
490    type Error = Infallible;
491
492    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
493        self.0.to_ascii().into_pyobject(py)
494    }
495}
496
497#[cfg(feature = "parquet")]
498impl<'py> FromPyObject<'py> for Wrap<StatisticsOptions> {
499    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
500        let mut statistics = StatisticsOptions::empty();
501
502        let dict = ob.downcast::<PyDict>()?;
503        for (key, val) in dict {
504            let key = key.extract::<PyBackedStr>()?;
505            let val = val.extract::<bool>()?;
506
507            match key.as_ref() {
508                "min" => statistics.min_value = val,
509                "max" => statistics.max_value = val,
510                "distinct_count" => statistics.distinct_count = val,
511                "null_count" => statistics.null_count = val,
512                _ => {
513                    return Err(PyTypeError::new_err(format!(
514                        "'{key}' is not a valid statistic option",
515                    )));
516                },
517            }
518        }
519
520        Ok(Wrap(statistics))
521    }
522}
523
524impl<'py> FromPyObject<'py> for Wrap<Row<'static>> {
525    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
526        let vals = ob.extract::<Vec<Wrap<AnyValue<'static>>>>()?;
527        let vals = reinterpret_vec(vals);
528        Ok(Wrap(Row(vals)))
529    }
530}
531
532impl<'py> FromPyObject<'py> for Wrap<Schema> {
533    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
534        let dict = ob.downcast::<PyDict>()?;
535
536        Ok(Wrap(
537            dict.iter()
538                .map(|(key, val)| {
539                    let key = key.extract::<PyBackedStr>()?;
540                    let val = val.extract::<Wrap<DataType>>()?;
541
542                    Ok(Field::new((&*key).into(), val.0))
543                })
544                .collect::<PyResult<Schema>>()?,
545        ))
546    }
547}
548
549impl<'py> FromPyObject<'py> for Wrap<ScanSources> {
550    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
551        let list = ob.downcast::<PyList>()?.to_owned();
552
553        if list.is_empty() {
554            return Ok(Wrap(ScanSources::default()));
555        }
556
557        enum MutableSources {
558            Paths(Vec<PathBuf>),
559            Files(Vec<File>),
560            Buffers(Vec<MemSlice>),
561        }
562
563        let num_items = list.len();
564        let mut iter = list
565            .into_iter()
566            .map(|val| get_python_scan_source_input(val.unbind(), false));
567
568        let Some(first) = iter.next() else {
569            return Ok(Wrap(ScanSources::default()));
570        };
571
572        let mut sources = match first? {
573            PythonScanSourceInput::Path(path) => {
574                let mut sources = Vec::with_capacity(num_items);
575                sources.push(path);
576                MutableSources::Paths(sources)
577            },
578            PythonScanSourceInput::File(file) => {
579                let mut sources = Vec::with_capacity(num_items);
580                sources.push(file.into());
581                MutableSources::Files(sources)
582            },
583            PythonScanSourceInput::Buffer(buffer) => {
584                let mut sources = Vec::with_capacity(num_items);
585                sources.push(buffer);
586                MutableSources::Buffers(sources)
587            },
588        };
589
590        for source in iter {
591            match (&mut sources, source?) {
592                (MutableSources::Paths(v), PythonScanSourceInput::Path(p)) => v.push(p),
593                (MutableSources::Files(v), PythonScanSourceInput::File(f)) => v.push(f.into()),
594                (MutableSources::Buffers(v), PythonScanSourceInput::Buffer(f)) => v.push(f),
595                _ => {
596                    return Err(PyTypeError::new_err(
597                        "Cannot combine in-memory bytes, paths and files for scan sources",
598                    ));
599                },
600            }
601        }
602
603        Ok(Wrap(match sources {
604            MutableSources::Paths(i) => ScanSources::Paths(i.into()),
605            MutableSources::Files(i) => ScanSources::Files(i.into()),
606            MutableSources::Buffers(i) => ScanSources::Buffers(i.into()),
607        }))
608    }
609}
610
611impl<'py> IntoPyObject<'py> for Wrap<&Schema> {
612    type Target = PyDict;
613    type Output = Bound<'py, Self::Target>;
614    type Error = PyErr;
615
616    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
617        let dict = PyDict::new(py);
618        self.0
619            .iter()
620            .try_for_each(|(k, v)| dict.set_item(k.as_str(), &Wrap(v.clone())))?;
621        Ok(dict)
622    }
623}
624
625#[derive(Debug)]
626#[repr(transparent)]
627pub struct ObjectValue {
628    pub inner: PyObject,
629}
630
631impl Clone for ObjectValue {
632    fn clone(&self) -> Self {
633        Python::with_gil(|py| Self {
634            inner: self.inner.clone_ref(py),
635        })
636    }
637}
638
639impl Hash for ObjectValue {
640    fn hash<H: Hasher>(&self, state: &mut H) {
641        let h = Python::with_gil(|py| self.inner.bind(py).hash().expect("should be hashable"));
642        state.write_isize(h)
643    }
644}
645
646impl Eq for ObjectValue {}
647
648impl PartialEq for ObjectValue {
649    fn eq(&self, other: &Self) -> bool {
650        Python::with_gil(|py| {
651            match self
652                .inner
653                .bind(py)
654                .rich_compare(other.inner.bind(py), CompareOp::Eq)
655            {
656                Ok(result) => result.is_truthy().unwrap(),
657                Err(_) => false,
658            }
659        })
660    }
661}
662
663impl TotalEq for ObjectValue {
664    fn tot_eq(&self, other: &Self) -> bool {
665        self == other
666    }
667}
668
669impl TotalHash for ObjectValue {
670    fn tot_hash<H>(&self, state: &mut H)
671    where
672        H: Hasher,
673    {
674        self.hash(state);
675    }
676}
677
678impl Display for ObjectValue {
679    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
680        write!(f, "{}", self.inner)
681    }
682}
683
684#[cfg(feature = "object")]
685impl PolarsObject for ObjectValue {
686    fn type_name() -> &'static str {
687        "object"
688    }
689}
690
691impl From<PyObject> for ObjectValue {
692    fn from(p: PyObject) -> Self {
693        Self { inner: p }
694    }
695}
696
697impl<'py> FromPyObject<'py> for ObjectValue {
698    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
699        Ok(ObjectValue {
700            inner: ob.to_owned().unbind(),
701        })
702    }
703}
704
705/// # Safety
706///
707/// The caller is responsible for checking that val is Object otherwise UB
708#[cfg(feature = "object")]
709impl From<&dyn PolarsObjectSafe> for &ObjectValue {
710    fn from(val: &dyn PolarsObjectSafe) -> Self {
711        unsafe { &*(val as *const dyn PolarsObjectSafe as *const ObjectValue) }
712    }
713}
714
715impl<'a, 'py> IntoPyObject<'py> for &'a ObjectValue {
716    type Target = PyAny;
717    type Output = Borrowed<'a, 'py, Self::Target>;
718    type Error = std::convert::Infallible;
719
720    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
721        Ok(self.inner.bind_borrowed(py))
722    }
723}
724
725impl Default for ObjectValue {
726    fn default() -> Self {
727        Python::with_gil(|py| ObjectValue { inner: py.None() })
728    }
729}
730
731impl<'py, T: NativeType + FromPyObject<'py>> FromPyObject<'py> for Wrap<Vec<T>> {
732    fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult<Self> {
733        let seq = obj.downcast::<PySequence>()?;
734        let mut v = Vec::with_capacity(seq.len().unwrap_or(0));
735        for item in seq.try_iter()? {
736            v.push(item?.extract::<T>()?);
737        }
738        Ok(Wrap(v))
739    }
740}
741
742#[cfg(feature = "asof_join")]
743impl<'py> FromPyObject<'py> for Wrap<AsofStrategy> {
744    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
745        let parsed = match &*(ob.extract::<PyBackedStr>()?) {
746            "backward" => AsofStrategy::Backward,
747            "forward" => AsofStrategy::Forward,
748            "nearest" => AsofStrategy::Nearest,
749            v => {
750                return Err(PyValueError::new_err(format!(
751                    "asof `strategy` must be one of {{'backward', 'forward', 'nearest'}}, got {v}",
752                )));
753            },
754        };
755        Ok(Wrap(parsed))
756    }
757}
758
759impl<'py> FromPyObject<'py> for Wrap<InterpolationMethod> {
760    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
761        let parsed = match &*(ob.extract::<PyBackedStr>()?) {
762            "linear" => InterpolationMethod::Linear,
763            "nearest" => InterpolationMethod::Nearest,
764            v => {
765                return Err(PyValueError::new_err(format!(
766                    "interpolation `method` must be one of {{'linear', 'nearest'}}, got {v}",
767                )));
768            },
769        };
770        Ok(Wrap(parsed))
771    }
772}
773
774#[cfg(feature = "avro")]
775impl<'py> FromPyObject<'py> for Wrap<Option<AvroCompression>> {
776    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
777        let parsed = match &*ob.extract::<PyBackedStr>()? {
778            "uncompressed" => None,
779            "snappy" => Some(AvroCompression::Snappy),
780            "deflate" => Some(AvroCompression::Deflate),
781            v => {
782                return Err(PyValueError::new_err(format!(
783                    "avro `compression` must be one of {{'uncompressed', 'snappy', 'deflate'}}, got {v}",
784                )));
785            },
786        };
787        Ok(Wrap(parsed))
788    }
789}
790
791impl<'py> FromPyObject<'py> for Wrap<CategoricalOrdering> {
792    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
793        let parsed = match &*ob.extract::<PyBackedStr>()? {
794            "physical" => CategoricalOrdering::Physical,
795            "lexical" => CategoricalOrdering::Lexical,
796            v => {
797                return Err(PyValueError::new_err(format!(
798                    "categorical `ordering` must be one of {{'physical', 'lexical'}}, got {v}",
799                )));
800            },
801        };
802        Ok(Wrap(parsed))
803    }
804}
805
806impl<'py> FromPyObject<'py> for Wrap<StartBy> {
807    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
808        let parsed = match &*ob.extract::<PyBackedStr>()? {
809            "window" => StartBy::WindowBound,
810            "datapoint" => StartBy::DataPoint,
811            "monday" => StartBy::Monday,
812            "tuesday" => StartBy::Tuesday,
813            "wednesday" => StartBy::Wednesday,
814            "thursday" => StartBy::Thursday,
815            "friday" => StartBy::Friday,
816            "saturday" => StartBy::Saturday,
817            "sunday" => StartBy::Sunday,
818            v => {
819                return Err(PyValueError::new_err(format!(
820                    "`start_by` must be one of {{'window', 'datapoint', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'}}, got {v}",
821                )));
822            },
823        };
824        Ok(Wrap(parsed))
825    }
826}
827
828impl<'py> FromPyObject<'py> for Wrap<ClosedWindow> {
829    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
830        let parsed = match &*ob.extract::<PyBackedStr>()? {
831            "left" => ClosedWindow::Left,
832            "right" => ClosedWindow::Right,
833            "both" => ClosedWindow::Both,
834            "none" => ClosedWindow::None,
835            v => {
836                return Err(PyValueError::new_err(format!(
837                    "`closed` must be one of {{'left', 'right', 'both', 'none'}}, got {v}",
838                )));
839            },
840        };
841        Ok(Wrap(parsed))
842    }
843}
844
845impl<'py> FromPyObject<'py> for Wrap<RoundMode> {
846    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
847        let parsed = match &*ob.extract::<PyBackedStr>()? {
848            "half_to_even" => RoundMode::HalfToEven,
849            "half_away_from_zero" => RoundMode::HalfAwayFromZero,
850            v => {
851                return Err(PyValueError::new_err(format!(
852                    "`mode` must be one of {{'half_to_even', 'half_away_from_zero'}}, got {v}",
853                )));
854            },
855        };
856        Ok(Wrap(parsed))
857    }
858}
859
860#[cfg(feature = "csv")]
861impl<'py> FromPyObject<'py> for Wrap<CsvEncoding> {
862    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
863        let parsed = match &*ob.extract::<PyBackedStr>()? {
864            "utf8" => CsvEncoding::Utf8,
865            "utf8-lossy" => CsvEncoding::LossyUtf8,
866            v => {
867                return Err(PyValueError::new_err(format!(
868                    "csv `encoding` must be one of {{'utf8', 'utf8-lossy'}}, got {v}",
869                )));
870            },
871        };
872        Ok(Wrap(parsed))
873    }
874}
875
876#[cfg(feature = "ipc")]
877impl<'py> FromPyObject<'py> for Wrap<Option<IpcCompression>> {
878    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
879        let parsed = match &*ob.extract::<PyBackedStr>()? {
880            "uncompressed" => None,
881            "lz4" => Some(IpcCompression::LZ4),
882            "zstd" => Some(IpcCompression::ZSTD),
883            v => {
884                return Err(PyValueError::new_err(format!(
885                    "ipc `compression` must be one of {{'uncompressed', 'lz4', 'zstd'}}, got {v}",
886                )));
887            },
888        };
889        Ok(Wrap(parsed))
890    }
891}
892
893impl<'py> FromPyObject<'py> for Wrap<JoinType> {
894    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
895        let parsed = match &*ob.extract::<PyBackedStr>()? {
896            "inner" => JoinType::Inner,
897            "left" => JoinType::Left,
898            "right" => JoinType::Right,
899            "full" => JoinType::Full,
900            "semi" => JoinType::Semi,
901            "anti" => JoinType::Anti,
902            #[cfg(feature = "cross_join")]
903            "cross" => JoinType::Cross,
904            v => {
905                return Err(PyValueError::new_err(format!(
906                    "`how` must be one of {{'inner', 'left', 'full', 'semi', 'anti', 'cross'}}, got {v}",
907                )));
908            },
909        };
910        Ok(Wrap(parsed))
911    }
912}
913
914impl<'py> FromPyObject<'py> for Wrap<Label> {
915    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
916        let parsed = match &*ob.extract::<PyBackedStr>()? {
917            "left" => Label::Left,
918            "right" => Label::Right,
919            "datapoint" => Label::DataPoint,
920            v => {
921                return Err(PyValueError::new_err(format!(
922                    "`label` must be one of {{'left', 'right', 'datapoint'}}, got {v}",
923                )));
924            },
925        };
926        Ok(Wrap(parsed))
927    }
928}
929
930impl<'py> FromPyObject<'py> for Wrap<ListToStructWidthStrategy> {
931    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
932        let parsed = match &*ob.extract::<PyBackedStr>()? {
933            "first_non_null" => ListToStructWidthStrategy::FirstNonNull,
934            "max_width" => ListToStructWidthStrategy::MaxWidth,
935            v => {
936                return Err(PyValueError::new_err(format!(
937                    "`n_field_strategy` must be one of {{'first_non_null', 'max_width'}}, got {v}",
938                )));
939            },
940        };
941        Ok(Wrap(parsed))
942    }
943}
944
945impl<'py> FromPyObject<'py> for Wrap<NonExistent> {
946    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
947        let parsed = match &*ob.extract::<PyBackedStr>()? {
948            "null" => NonExistent::Null,
949            "raise" => NonExistent::Raise,
950            v => {
951                return Err(PyValueError::new_err(format!(
952                    "`non_existent` must be one of {{'null', 'raise'}}, got {v}",
953                )));
954            },
955        };
956        Ok(Wrap(parsed))
957    }
958}
959
960impl<'py> FromPyObject<'py> for Wrap<NullBehavior> {
961    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
962        let parsed = match &*ob.extract::<PyBackedStr>()? {
963            "drop" => NullBehavior::Drop,
964            "ignore" => NullBehavior::Ignore,
965            v => {
966                return Err(PyValueError::new_err(format!(
967                    "`null_behavior` must be one of {{'drop', 'ignore'}}, got {v}",
968                )));
969            },
970        };
971        Ok(Wrap(parsed))
972    }
973}
974
975impl<'py> FromPyObject<'py> for Wrap<NullStrategy> {
976    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
977        let parsed = match &*ob.extract::<PyBackedStr>()? {
978            "ignore" => NullStrategy::Ignore,
979            "propagate" => NullStrategy::Propagate,
980            v => {
981                return Err(PyValueError::new_err(format!(
982                    "`null_strategy` must be one of {{'ignore', 'propagate'}}, got {v}",
983                )));
984            },
985        };
986        Ok(Wrap(parsed))
987    }
988}
989
990#[cfg(feature = "parquet")]
991impl<'py> FromPyObject<'py> for Wrap<ParallelStrategy> {
992    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
993        let parsed = match &*ob.extract::<PyBackedStr>()? {
994            "auto" => ParallelStrategy::Auto,
995            "columns" => ParallelStrategy::Columns,
996            "row_groups" => ParallelStrategy::RowGroups,
997            "prefiltered" => ParallelStrategy::Prefiltered,
998            "none" => ParallelStrategy::None,
999            v => {
1000                return Err(PyValueError::new_err(format!(
1001                    "`parallel` must be one of {{'auto', 'columns', 'row_groups', 'prefiltered', 'none'}}, got {v}",
1002                )));
1003            },
1004        };
1005        Ok(Wrap(parsed))
1006    }
1007}
1008
1009impl<'py> FromPyObject<'py> for Wrap<IndexOrder> {
1010    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1011        let parsed = match &*ob.extract::<PyBackedStr>()? {
1012            "fortran" => IndexOrder::Fortran,
1013            "c" => IndexOrder::C,
1014            v => {
1015                return Err(PyValueError::new_err(format!(
1016                    "`order` must be one of {{'fortran', 'c'}}, got {v}",
1017                )));
1018            },
1019        };
1020        Ok(Wrap(parsed))
1021    }
1022}
1023
1024impl<'py> FromPyObject<'py> for Wrap<QuantileMethod> {
1025    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1026        let parsed = match &*ob.extract::<PyBackedStr>()? {
1027            "lower" => QuantileMethod::Lower,
1028            "higher" => QuantileMethod::Higher,
1029            "nearest" => QuantileMethod::Nearest,
1030            "linear" => QuantileMethod::Linear,
1031            "midpoint" => QuantileMethod::Midpoint,
1032            "equiprobable" => QuantileMethod::Equiprobable,
1033            v => {
1034                return Err(PyValueError::new_err(format!(
1035                    "`interpolation` must be one of {{'lower', 'higher', 'nearest', 'linear', 'midpoint', 'equiprobable'}}, got {v}",
1036                )));
1037            },
1038        };
1039        Ok(Wrap(parsed))
1040    }
1041}
1042
1043impl<'py> FromPyObject<'py> for Wrap<RankMethod> {
1044    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1045        let parsed = match &*ob.extract::<PyBackedStr>()? {
1046            "min" => RankMethod::Min,
1047            "max" => RankMethod::Max,
1048            "average" => RankMethod::Average,
1049            "dense" => RankMethod::Dense,
1050            "ordinal" => RankMethod::Ordinal,
1051            "random" => RankMethod::Random,
1052            v => {
1053                return Err(PyValueError::new_err(format!(
1054                    "rank `method` must be one of {{'min', 'max', 'average', 'dense', 'ordinal', 'random'}}, got {v}",
1055                )));
1056            },
1057        };
1058        Ok(Wrap(parsed))
1059    }
1060}
1061
1062impl<'py> FromPyObject<'py> for Wrap<Roll> {
1063    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1064        let parsed = match &*ob.extract::<PyBackedStr>()? {
1065            "raise" => Roll::Raise,
1066            "forward" => Roll::Forward,
1067            "backward" => Roll::Backward,
1068            v => {
1069                return Err(PyValueError::new_err(format!(
1070                    "`roll` must be one of {{'raise', 'forward', 'backward'}}, got {v}",
1071                )));
1072            },
1073        };
1074        Ok(Wrap(parsed))
1075    }
1076}
1077
1078impl<'py> FromPyObject<'py> for Wrap<TimeUnit> {
1079    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1080        let parsed = match &*ob.extract::<PyBackedStr>()? {
1081            "ns" => TimeUnit::Nanoseconds,
1082            "us" => TimeUnit::Microseconds,
1083            "ms" => TimeUnit::Milliseconds,
1084            v => {
1085                return Err(PyValueError::new_err(format!(
1086                    "`time_unit` must be one of {{'ns', 'us', 'ms'}}, got {v}",
1087                )));
1088            },
1089        };
1090        Ok(Wrap(parsed))
1091    }
1092}
1093
1094impl<'py> FromPyObject<'py> for Wrap<UniqueKeepStrategy> {
1095    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1096        let parsed = match &*ob.extract::<PyBackedStr>()? {
1097            "first" => UniqueKeepStrategy::First,
1098            "last" => UniqueKeepStrategy::Last,
1099            "none" => UniqueKeepStrategy::None,
1100            "any" => UniqueKeepStrategy::Any,
1101            v => {
1102                return Err(PyValueError::new_err(format!(
1103                    "`keep` must be one of {{'first', 'last', 'any', 'none'}}, got {v}",
1104                )));
1105            },
1106        };
1107        Ok(Wrap(parsed))
1108    }
1109}
1110
1111#[cfg(feature = "search_sorted")]
1112impl<'py> FromPyObject<'py> for Wrap<SearchSortedSide> {
1113    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1114        let parsed = match &*ob.extract::<PyBackedStr>()? {
1115            "any" => SearchSortedSide::Any,
1116            "left" => SearchSortedSide::Left,
1117            "right" => SearchSortedSide::Right,
1118            v => {
1119                return Err(PyValueError::new_err(format!(
1120                    "sorted `side` must be one of {{'any', 'left', 'right'}}, got {v}",
1121                )));
1122            },
1123        };
1124        Ok(Wrap(parsed))
1125    }
1126}
1127
1128impl<'py> FromPyObject<'py> for Wrap<ClosedInterval> {
1129    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1130        let parsed = match &*ob.extract::<PyBackedStr>()? {
1131            "both" => ClosedInterval::Both,
1132            "left" => ClosedInterval::Left,
1133            "right" => ClosedInterval::Right,
1134            "none" => ClosedInterval::None,
1135            v => {
1136                return Err(PyValueError::new_err(format!(
1137                    "`closed` must be one of {{'both', 'left', 'right', 'none'}}, got {v}",
1138                )));
1139            },
1140        };
1141        Ok(Wrap(parsed))
1142    }
1143}
1144
1145impl<'py> FromPyObject<'py> for Wrap<WindowMapping> {
1146    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1147        let parsed = match &*ob.extract::<PyBackedStr>()? {
1148            "group_to_rows" => WindowMapping::GroupsToRows,
1149            "join" => WindowMapping::Join,
1150            "explode" => WindowMapping::Explode,
1151            v => {
1152                return Err(PyValueError::new_err(format!(
1153                    "`mapping_strategy` must be one of {{'group_to_rows', 'join', 'explode'}}, got {v}",
1154                )));
1155            },
1156        };
1157        Ok(Wrap(parsed))
1158    }
1159}
1160
1161impl<'py> FromPyObject<'py> for Wrap<JoinValidation> {
1162    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1163        let parsed = match &*ob.extract::<PyBackedStr>()? {
1164            "1:1" => JoinValidation::OneToOne,
1165            "1:m" => JoinValidation::OneToMany,
1166            "m:m" => JoinValidation::ManyToMany,
1167            "m:1" => JoinValidation::ManyToOne,
1168            v => {
1169                return Err(PyValueError::new_err(format!(
1170                    "`validate` must be one of {{'m:m', 'm:1', '1:m', '1:1'}}, got {v}",
1171                )));
1172            },
1173        };
1174        Ok(Wrap(parsed))
1175    }
1176}
1177
1178impl<'py> FromPyObject<'py> for Wrap<MaintainOrderJoin> {
1179    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1180        let parsed = match &*ob.extract::<PyBackedStr>()? {
1181            "none" => MaintainOrderJoin::None,
1182            "left" => MaintainOrderJoin::Left,
1183            "right" => MaintainOrderJoin::Right,
1184            "left_right" => MaintainOrderJoin::LeftRight,
1185            "right_left" => MaintainOrderJoin::RightLeft,
1186            v => {
1187                return Err(PyValueError::new_err(format!(
1188                    "`maintain_order` must be one of {{'none', 'left', 'right', 'left_right', 'right_left'}}, got {v}",
1189                )));
1190            },
1191        };
1192        Ok(Wrap(parsed))
1193    }
1194}
1195
1196#[cfg(feature = "csv")]
1197impl<'py> FromPyObject<'py> for Wrap<QuoteStyle> {
1198    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1199        let parsed = match &*ob.extract::<PyBackedStr>()? {
1200            "always" => QuoteStyle::Always,
1201            "necessary" => QuoteStyle::Necessary,
1202            "non_numeric" => QuoteStyle::NonNumeric,
1203            "never" => QuoteStyle::Never,
1204            v => {
1205                return Err(PyValueError::new_err(format!(
1206                    "`quote_style` must be one of {{'always', 'necessary', 'non_numeric', 'never'}}, got {v}",
1207                )));
1208            },
1209        };
1210        Ok(Wrap(parsed))
1211    }
1212}
1213
1214#[cfg(feature = "cloud")]
1215pub(crate) fn parse_cloud_options(uri: &str, kv: Vec<(String, String)>) -> PyResult<CloudOptions> {
1216    let out = CloudOptions::from_untyped_config(uri, kv).map_err(PyPolarsErr::from)?;
1217    Ok(out)
1218}
1219
1220#[cfg(feature = "list_sets")]
1221impl<'py> FromPyObject<'py> for Wrap<SetOperation> {
1222    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1223        let parsed = match &*ob.extract::<PyBackedStr>()? {
1224            "union" => SetOperation::Union,
1225            "difference" => SetOperation::Difference,
1226            "intersection" => SetOperation::Intersection,
1227            "symmetric_difference" => SetOperation::SymmetricDifference,
1228            v => {
1229                return Err(PyValueError::new_err(format!(
1230                    "set operation must be one of {{'union', 'difference', 'intersection', 'symmetric_difference'}}, got {v}",
1231                )));
1232            },
1233        };
1234        Ok(Wrap(parsed))
1235    }
1236}
1237
1238// Conversion from ScanCastOptions class from the Python side.
1239impl<'py> FromPyObject<'py> for Wrap<CastColumnsPolicy> {
1240    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1241        if ob.is_none() {
1242            // Initialize the default ScanCastOptions from Python.
1243
1244            static DEFAULT: GILOnceCell<Wrap<CastColumnsPolicy>> = GILOnceCell::new();
1245
1246            let out = DEFAULT.get_or_try_init(ob.py(), || {
1247                let ob = PyModule::import(ob.py(), "polars.io.cast_options")
1248                    .unwrap()
1249                    .getattr("ScanCastOptions")
1250                    .unwrap()
1251                    .call_method0("_default")
1252                    .unwrap();
1253
1254                let out = Self::extract_bound(&ob)?;
1255
1256                // The default policy should match ERROR_ON_MISMATCH (but this can change).
1257                debug_assert_eq!(&out.0, &CastColumnsPolicy::ERROR_ON_MISMATCH);
1258
1259                PyResult::Ok(out)
1260            })?;
1261
1262            return Ok(out.clone());
1263        }
1264
1265        let integer_upcast = match &*ob.getattr("integer_cast")?.extract::<PyBackedStr>()? {
1266            "upcast" => true,
1267            "forbid" => false,
1268            v => {
1269                return Err(PyValueError::new_err(format!(
1270                    "unknown option for integer_cast: {}",
1271                    v
1272                )));
1273            },
1274        };
1275
1276        let mut float_upcast = false;
1277        let mut float_downcast = false;
1278
1279        let mut parse_float_cast_option = |v: &str| -> PyResult<()> {
1280            match v {
1281                "forbid" => {},
1282                "upcast" => float_upcast = true,
1283                "downcast" => float_downcast = true,
1284                v => {
1285                    return Err(PyValueError::new_err(format!(
1286                        "unknown option for float_cast: {}",
1287                        v
1288                    )));
1289                },
1290            }
1291
1292            Ok(())
1293        };
1294
1295        let float_cast_object = ob.getattr("float_cast")?;
1296
1297        parse_multiple_options(
1298            "float_cast",
1299            float_cast_object,
1300            &mut parse_float_cast_option,
1301        )?;
1302
1303        let mut datetime_nanoseconds_downcast = false;
1304        let mut datetime_convert_timezone = false;
1305
1306        let mut parse_datetime_cast_option = |v: &str| -> PyResult<()> {
1307            match v {
1308                "forbid" => {},
1309                "nanosecond-downcast" => datetime_nanoseconds_downcast = true,
1310                "convert-timezone" => datetime_convert_timezone = true,
1311                v => {
1312                    return Err(PyValueError::new_err(format!(
1313                        "unknown option for datetime_cast: {}",
1314                        v
1315                    )));
1316                },
1317            };
1318
1319            Ok(())
1320        };
1321
1322        let datetime_cast_object = ob.getattr("datetime_cast")?;
1323
1324        parse_multiple_options(
1325            "datetime_cast",
1326            datetime_cast_object,
1327            &mut parse_datetime_cast_option,
1328        )?;
1329
1330        let missing_struct_fields = match &*ob
1331            .getattr("missing_struct_fields")?
1332            .extract::<PyBackedStr>()?
1333        {
1334            "insert" => MissingColumnsPolicy::Insert,
1335            "raise" => MissingColumnsPolicy::Raise,
1336            v => {
1337                return Err(PyValueError::new_err(format!(
1338                    "unknown option for missing_struct_fields: {}",
1339                    v
1340                )));
1341            },
1342        };
1343
1344        let extra_struct_fields = match &*ob
1345            .getattr("extra_struct_fields")?
1346            .extract::<PyBackedStr>()?
1347        {
1348            "ignore" => ExtraColumnsPolicy::Ignore,
1349            "raise" => ExtraColumnsPolicy::Raise,
1350            v => {
1351                return Err(PyValueError::new_err(format!(
1352                    "unknown option for extra_struct_fields: {}",
1353                    v
1354                )));
1355            },
1356        };
1357
1358        return Ok(Wrap(CastColumnsPolicy {
1359            integer_upcast,
1360            float_upcast,
1361            float_downcast,
1362            datetime_nanoseconds_downcast,
1363            datetime_microseconds_downcast: false,
1364            datetime_convert_timezone,
1365            missing_struct_fields,
1366            extra_struct_fields,
1367        }));
1368
1369        fn parse_multiple_options<'a>(
1370            parameter_name: &'static str,
1371            py_object: Bound<'a, PyAny>,
1372            parser_func: &mut dyn FnMut(&str) -> PyResult<()>,
1373        ) -> PyResult<()> {
1374            if let Ok(v) = py_object.extract::<PyBackedStr>() {
1375                parser_func(&v)?;
1376            } else if let Ok(v) = py_object.try_iter() {
1377                for v in v {
1378                    parser_func(&v?.extract::<PyBackedStr>()?)?;
1379                }
1380            } else {
1381                return Err(PyValueError::new_err(format!(
1382                    "unknown type for {}: {}",
1383                    parameter_name, py_object
1384                )));
1385            }
1386
1387            Ok(())
1388        }
1389    }
1390}
1391
1392pub(crate) fn parse_fill_null_strategy(
1393    strategy: &str,
1394    limit: FillNullLimit,
1395) -> PyResult<FillNullStrategy> {
1396    let parsed = match strategy {
1397        "forward" => FillNullStrategy::Forward(limit),
1398        "backward" => FillNullStrategy::Backward(limit),
1399        "min" => FillNullStrategy::Min,
1400        "max" => FillNullStrategy::Max,
1401        "mean" => FillNullStrategy::Mean,
1402        "zero" => FillNullStrategy::Zero,
1403        "one" => FillNullStrategy::One,
1404        e => {
1405            return Err(PyValueError::new_err(format!(
1406                "`strategy` must be one of {{'forward', 'backward', 'min', 'max', 'mean', 'zero', 'one'}}, got {e}",
1407            )));
1408        },
1409    };
1410    Ok(parsed)
1411}
1412
1413#[cfg(feature = "parquet")]
1414pub(crate) fn parse_parquet_compression(
1415    compression: &str,
1416    compression_level: Option<i32>,
1417) -> PyResult<ParquetCompression> {
1418    let parsed = match compression {
1419        "uncompressed" => ParquetCompression::Uncompressed,
1420        "snappy" => ParquetCompression::Snappy,
1421        "gzip" => ParquetCompression::Gzip(
1422            compression_level
1423                .map(|lvl| {
1424                    GzipLevel::try_new(lvl as u8)
1425                        .map_err(|e| PyValueError::new_err(format!("{e:?}")))
1426                })
1427                .transpose()?,
1428        ),
1429        "lzo" => ParquetCompression::Lzo,
1430        "brotli" => ParquetCompression::Brotli(
1431            compression_level
1432                .map(|lvl| {
1433                    BrotliLevel::try_new(lvl as u32)
1434                        .map_err(|e| PyValueError::new_err(format!("{e:?}")))
1435                })
1436                .transpose()?,
1437        ),
1438        "lz4" => ParquetCompression::Lz4Raw,
1439        "zstd" => ParquetCompression::Zstd(
1440            compression_level
1441                .map(|lvl| {
1442                    ZstdLevel::try_new(lvl).map_err(|e| PyValueError::new_err(format!("{e:?}")))
1443                })
1444                .transpose()?,
1445        ),
1446        e => {
1447            return Err(PyValueError::new_err(format!(
1448                "parquet `compression` must be one of {{'uncompressed', 'snappy', 'gzip', 'lzo', 'brotli', 'lz4', 'zstd'}}, got {e}",
1449            )));
1450        },
1451    };
1452    Ok(parsed)
1453}
1454
1455pub(crate) fn strings_to_pl_smallstr<I, S>(container: I) -> Vec<PlSmallStr>
1456where
1457    I: IntoIterator<Item = S>,
1458    S: AsRef<str>,
1459{
1460    container
1461        .into_iter()
1462        .map(|s| PlSmallStr::from_str(s.as_ref()))
1463        .collect()
1464}
1465
1466#[derive(Debug, Copy, Clone)]
1467pub struct PyCompatLevel(pub CompatLevel);
1468
1469impl<'py> FromPyObject<'py> for PyCompatLevel {
1470    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1471        Ok(PyCompatLevel(if let Ok(level) = ob.extract::<u16>() {
1472            if let Ok(compat_level) = CompatLevel::with_level(level) {
1473                compat_level
1474            } else {
1475                return Err(PyValueError::new_err("invalid compat level"));
1476            }
1477        } else if let Ok(future) = ob.extract::<bool>() {
1478            if future {
1479                CompatLevel::newest()
1480            } else {
1481                CompatLevel::oldest()
1482            }
1483        } else {
1484            return Err(PyTypeError::new_err(
1485                "'compat_level' argument accepts int or bool",
1486            ));
1487        }))
1488    }
1489}
1490
1491#[cfg(feature = "string_normalize")]
1492impl<'py> FromPyObject<'py> for Wrap<UnicodeForm> {
1493    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1494        let parsed = match &*ob.extract::<PyBackedStr>()? {
1495            "NFC" => UnicodeForm::NFC,
1496            "NFKC" => UnicodeForm::NFKC,
1497            "NFD" => UnicodeForm::NFD,
1498            "NFKD" => UnicodeForm::NFKD,
1499            v => {
1500                return Err(PyValueError::new_err(format!(
1501                    "`form` must be one of {{'NFC', 'NFKC', 'NFD', 'NFKD'}}, got {v}",
1502                )));
1503            },
1504        };
1505        Ok(Wrap(parsed))
1506    }
1507}
1508
1509#[cfg(feature = "parquet")]
1510impl<'py> FromPyObject<'py> for Wrap<Option<KeyValueMetadata>> {
1511    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1512        #[derive(FromPyObject)]
1513        enum Metadata {
1514            Static(Vec<(String, String)>),
1515            Dynamic(PyObject),
1516        }
1517
1518        let metadata = Option::<Metadata>::extract_bound(ob)?;
1519        let key_value_metadata = metadata.map(|x| match x {
1520            Metadata::Static(kv) => KeyValueMetadata::from_static(kv),
1521            Metadata::Dynamic(func) => KeyValueMetadata::from_py_function(func),
1522        });
1523        Ok(Wrap(key_value_metadata))
1524    }
1525}
1526
1527impl<'py> FromPyObject<'py> for Wrap<Option<TimeZone>> {
1528    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1529        let tz = Option::<Wrap<PlSmallStr>>::extract_bound(ob)?;
1530
1531        let tz = tz.map(|x| x.0);
1532
1533        Ok(Wrap(TimeZone::opt_try_new(tz).map_err(to_py_err)?))
1534    }
1535}
1536
1537impl<'py> FromPyObject<'py> for Wrap<UpcastOrForbid> {
1538    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1539        let parsed = match &*ob.extract::<PyBackedStr>()? {
1540            "upcast" => UpcastOrForbid::Upcast,
1541            "forbid" => UpcastOrForbid::Forbid,
1542            v => {
1543                return Err(PyValueError::new_err(format!(
1544                    "cast parameter must be one of {{'upcast', 'forbid'}}, got {v}",
1545                )));
1546            },
1547        };
1548        Ok(Wrap(parsed))
1549    }
1550}
1551
1552impl<'py> FromPyObject<'py> for Wrap<ExtraColumnsPolicy> {
1553    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1554        let parsed = match &*ob.extract::<PyBackedStr>()? {
1555            "ignore" => ExtraColumnsPolicy::Ignore,
1556            "raise" => ExtraColumnsPolicy::Raise,
1557            v => {
1558                return Err(PyValueError::new_err(format!(
1559                    "extra column/field parameter must be one of {{'ignore', 'raise'}}, got {v}",
1560                )));
1561            },
1562        };
1563        Ok(Wrap(parsed))
1564    }
1565}
1566
1567impl<'py> FromPyObject<'py> for Wrap<MissingColumnsPolicy> {
1568    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1569        let parsed = match &*ob.extract::<PyBackedStr>()? {
1570            "insert" => MissingColumnsPolicy::Insert,
1571            "raise" => MissingColumnsPolicy::Raise,
1572            v => {
1573                return Err(PyValueError::new_err(format!(
1574                    "missing column/field parameter must be one of {{'insert', 'raise'}}, got {v}",
1575                )));
1576            },
1577        };
1578        Ok(Wrap(parsed))
1579    }
1580}
1581
1582impl<'py> FromPyObject<'py> for Wrap<MissingColumnsPolicyOrExpr> {
1583    fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult<Self> {
1584        if let Ok(pyexpr) = ob.extract::<PyExpr>() {
1585            return Ok(Wrap(MissingColumnsPolicyOrExpr::InsertWith(pyexpr.inner)));
1586        }
1587
1588        let parsed = match &*ob.extract::<PyBackedStr>()? {
1589            "insert" => MissingColumnsPolicyOrExpr::Insert,
1590            "raise" => MissingColumnsPolicyOrExpr::Raise,
1591            v => {
1592                return Err(PyValueError::new_err(format!(
1593                    "missing column/field parameter must be one of {{'insert', 'raise', expression}}, got {v}",
1594                )));
1595            },
1596        };
1597        Ok(Wrap(parsed))
1598    }
1599}