polars_python/lazyframe/visitor/
expr_nodes.rs

1#[cfg(feature = "iejoin")]
2use polars::prelude::InequalityOperator;
3use polars::series::ops::NullBehavior;
4use polars_core::chunked_array::ops::FillNullStrategy;
5use polars_core::series::IsSorted;
6#[cfg(feature = "string_normalize")]
7use polars_ops::chunked_array::UnicodeForm;
8use polars_ops::prelude::RankMethod;
9use polars_ops::series::InterpolationMethod;
10#[cfg(feature = "search_sorted")]
11use polars_ops::series::SearchSortedSide;
12use polars_plan::plans::{
13    DynLiteralValue, IRBooleanFunction, IRFunctionExpr, IRPowFunction, IRRollingFunctionBy,
14    IRStringFunction, IRStructFunction, IRTemporalFunction,
15};
16use polars_plan::prelude::{
17    AExpr, GroupbyOptions, IRAggExpr, LiteralValue, Operator, WindowMapping, WindowType,
18};
19use polars_time::prelude::RollingGroupOptions;
20use polars_time::{Duration, DynamicGroupOptions};
21use pyo3::IntoPyObjectExt;
22use pyo3::exceptions::PyNotImplementedError;
23use pyo3::prelude::*;
24use pyo3::types::{PyInt, PyTuple};
25
26use crate::Wrap;
27use crate::series::PySeries;
28
29#[pyclass(frozen)]
30pub struct Alias {
31    #[pyo3(get)]
32    expr: usize,
33    #[pyo3(get)]
34    name: PyObject,
35}
36
37#[pyclass(frozen)]
38pub struct Column {
39    #[pyo3(get)]
40    name: PyObject,
41}
42
43#[pyclass(frozen)]
44pub struct Literal {
45    #[pyo3(get)]
46    value: PyObject,
47    #[pyo3(get)]
48    dtype: PyObject,
49}
50
51#[pyclass(name = "Operator", eq, frozen)]
52#[derive(Copy, Clone, PartialEq)]
53pub enum PyOperator {
54    Eq,
55    EqValidity,
56    NotEq,
57    NotEqValidity,
58    Lt,
59    LtEq,
60    Gt,
61    GtEq,
62    Plus,
63    Minus,
64    Multiply,
65    Divide,
66    TrueDivide,
67    FloorDivide,
68    Modulus,
69    And,
70    Or,
71    Xor,
72    LogicalAnd,
73    LogicalOr,
74}
75
76#[pymethods]
77impl PyOperator {
78    fn __hash__(&self) -> isize {
79        *self as isize
80    }
81}
82
83impl<'py> IntoPyObject<'py> for Wrap<Operator> {
84    type Target = PyOperator;
85    type Output = Bound<'py, Self::Target>;
86    type Error = PyErr;
87
88    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
89        match self.0 {
90            Operator::Eq => PyOperator::Eq,
91            Operator::EqValidity => PyOperator::EqValidity,
92            Operator::NotEq => PyOperator::NotEq,
93            Operator::NotEqValidity => PyOperator::NotEqValidity,
94            Operator::Lt => PyOperator::Lt,
95            Operator::LtEq => PyOperator::LtEq,
96            Operator::Gt => PyOperator::Gt,
97            Operator::GtEq => PyOperator::GtEq,
98            Operator::Plus => PyOperator::Plus,
99            Operator::Minus => PyOperator::Minus,
100            Operator::Multiply => PyOperator::Multiply,
101            Operator::Divide => PyOperator::Divide,
102            Operator::TrueDivide => PyOperator::TrueDivide,
103            Operator::FloorDivide => PyOperator::FloorDivide,
104            Operator::Modulus => PyOperator::Modulus,
105            Operator::And => PyOperator::And,
106            Operator::Or => PyOperator::Or,
107            Operator::Xor => PyOperator::Xor,
108            Operator::LogicalAnd => PyOperator::LogicalAnd,
109            Operator::LogicalOr => PyOperator::LogicalOr,
110        }
111        .into_pyobject(py)
112    }
113}
114
115#[cfg(feature = "iejoin")]
116impl<'py> IntoPyObject<'py> for Wrap<InequalityOperator> {
117    type Target = PyOperator;
118    type Output = Bound<'py, Self::Target>;
119    type Error = PyErr;
120
121    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
122        match self.0 {
123            InequalityOperator::Lt => PyOperator::Lt,
124            InequalityOperator::LtEq => PyOperator::LtEq,
125            InequalityOperator::Gt => PyOperator::Gt,
126            InequalityOperator::GtEq => PyOperator::GtEq,
127        }
128        .into_pyobject(py)
129    }
130}
131
132#[pyclass(name = "StringFunction", eq, frozen)]
133#[derive(Copy, Clone, PartialEq)]
134pub enum PyStringFunction {
135    ConcatHorizontal,
136    ConcatVertical,
137    Contains,
138    CountMatches,
139    EndsWith,
140    Extract,
141    ExtractAll,
142    ExtractGroups,
143    Find,
144    ToInteger,
145    LenBytes,
146    LenChars,
147    Lowercase,
148    JsonDecode,
149    JsonPathMatch,
150    Replace,
151    Reverse,
152    PadStart,
153    PadEnd,
154    Slice,
155    Head,
156    Tail,
157    HexEncode,
158    HexDecode,
159    Base64Encode,
160    Base64Decode,
161    StartsWith,
162    StripChars,
163    StripCharsStart,
164    StripCharsEnd,
165    StripPrefix,
166    StripSuffix,
167    SplitExact,
168    SplitN,
169    Strptime,
170    Split,
171    ToDecimal,
172    Titlecase,
173    Uppercase,
174    ZFill,
175    ContainsAny,
176    ReplaceMany,
177    EscapeRegex,
178    Normalize,
179}
180
181#[pymethods]
182impl PyStringFunction {
183    fn __hash__(&self) -> isize {
184        *self as isize
185    }
186}
187
188#[pyclass(name = "BooleanFunction", eq, frozen)]
189#[derive(Copy, Clone, PartialEq)]
190pub enum PyBooleanFunction {
191    Any,
192    All,
193    IsNull,
194    IsNotNull,
195    IsFinite,
196    IsInfinite,
197    IsNan,
198    IsNotNan,
199    IsFirstDistinct,
200    IsLastDistinct,
201    IsUnique,
202    IsDuplicated,
203    IsBetween,
204    IsIn,
205    IsClose,
206    AllHorizontal,
207    AnyHorizontal,
208    Not,
209}
210
211#[pymethods]
212impl PyBooleanFunction {
213    fn __hash__(&self) -> isize {
214        *self as isize
215    }
216}
217
218#[pyclass(name = "TemporalFunction", eq, frozen)]
219#[derive(Copy, Clone, PartialEq)]
220pub enum PyTemporalFunction {
221    Millennium,
222    Century,
223    Year,
224    IsLeapYear,
225    IsoYear,
226    Quarter,
227    Month,
228    DaysInMonth,
229    Week,
230    WeekDay,
231    Day,
232    OrdinalDay,
233    Time,
234    Date,
235    Datetime,
236    Duration,
237    Hour,
238    Minute,
239    Second,
240    Millisecond,
241    Microsecond,
242    Nanosecond,
243    TotalDays,
244    TotalHours,
245    TotalMinutes,
246    TotalSeconds,
247    TotalMilliseconds,
248    TotalMicroseconds,
249    TotalNanoseconds,
250    ToString,
251    CastTimeUnit,
252    WithTimeUnit,
253    ConvertTimeZone,
254    TimeStamp,
255    Truncate,
256    OffsetBy,
257    MonthStart,
258    MonthEnd,
259    BaseUtcOffset,
260    DSTOffset,
261    Round,
262    Replace,
263    ReplaceTimeZone,
264    Combine,
265    DatetimeFunction,
266}
267
268#[pymethods]
269impl PyTemporalFunction {
270    fn __hash__(&self) -> isize {
271        *self as isize
272    }
273}
274
275#[pyclass(name = "StructFunction", eq, frozen)]
276#[derive(Copy, Clone, PartialEq)]
277pub enum PyStructFunction {
278    FieldByName,
279    RenameFields,
280    PrefixFields,
281    SuffixFields,
282    JsonEncode,
283    WithFields,
284    MapFieldNames,
285}
286
287#[pymethods]
288impl PyStructFunction {
289    fn __hash__(&self) -> isize {
290        *self as isize
291    }
292}
293
294#[pyclass(frozen)]
295pub struct BinaryExpr {
296    #[pyo3(get)]
297    left: usize,
298    #[pyo3(get)]
299    op: PyObject,
300    #[pyo3(get)]
301    right: usize,
302}
303
304#[pyclass(frozen)]
305pub struct Cast {
306    #[pyo3(get)]
307    expr: usize,
308    #[pyo3(get)]
309    dtype: PyObject,
310    // 0: strict
311    // 1: non-strict
312    // 2: overflow
313    #[pyo3(get)]
314    options: u8,
315}
316
317#[pyclass(frozen)]
318pub struct Sort {
319    #[pyo3(get)]
320    expr: usize,
321    #[pyo3(get)]
322    /// maintain_order, nulls_last, descending
323    options: (bool, bool, bool),
324}
325
326#[pyclass(frozen)]
327pub struct Gather {
328    #[pyo3(get)]
329    expr: usize,
330    #[pyo3(get)]
331    idx: usize,
332    #[pyo3(get)]
333    scalar: bool,
334}
335
336#[pyclass(frozen)]
337pub struct Filter {
338    #[pyo3(get)]
339    input: usize,
340    #[pyo3(get)]
341    by: usize,
342}
343
344#[pyclass(frozen)]
345pub struct SortBy {
346    #[pyo3(get)]
347    expr: usize,
348    #[pyo3(get)]
349    by: Vec<usize>,
350    #[pyo3(get)]
351    /// maintain_order, nulls_last, descending
352    sort_options: (bool, Vec<bool>, Vec<bool>),
353}
354
355#[pyclass(frozen)]
356pub struct Agg {
357    #[pyo3(get)]
358    name: PyObject,
359    #[pyo3(get)]
360    arguments: Vec<usize>,
361    #[pyo3(get)]
362    // Arbitrary control options
363    options: PyObject,
364}
365
366#[pyclass(frozen)]
367pub struct Ternary {
368    #[pyo3(get)]
369    predicate: usize,
370    #[pyo3(get)]
371    truthy: usize,
372    #[pyo3(get)]
373    falsy: usize,
374}
375
376#[pyclass(frozen)]
377pub struct Function {
378    #[pyo3(get)]
379    input: Vec<usize>,
380    #[pyo3(get)]
381    function_data: PyObject,
382    #[pyo3(get)]
383    options: PyObject,
384}
385
386#[pyclass(frozen)]
387pub struct Slice {
388    #[pyo3(get)]
389    input: usize,
390    #[pyo3(get)]
391    offset: usize,
392    #[pyo3(get)]
393    length: usize,
394}
395
396#[pyclass(frozen)]
397pub struct Len {}
398
399#[pyclass(frozen)]
400pub struct Window {
401    #[pyo3(get)]
402    function: usize,
403    #[pyo3(get)]
404    partition_by: Vec<usize>,
405    #[pyo3(get)]
406    order_by: Option<usize>,
407    #[pyo3(get)]
408    order_by_descending: bool,
409    #[pyo3(get)]
410    order_by_nulls_last: bool,
411    #[pyo3(get)]
412    options: PyObject,
413}
414
415#[pyclass(name = "WindowMapping", frozen)]
416pub struct PyWindowMapping {
417    inner: WindowMapping,
418}
419
420#[pymethods]
421impl PyWindowMapping {
422    #[getter]
423    fn kind(&self) -> &str {
424        self.inner.into()
425    }
426}
427
428impl<'py> IntoPyObject<'py> for Wrap<Duration> {
429    type Target = PyTuple;
430    type Output = Bound<'py, Self::Target>;
431    type Error = PyErr;
432
433    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
434        (
435            self.0.months(),
436            self.0.weeks(),
437            self.0.days(),
438            self.0.nanoseconds(),
439            self.0.parsed_int,
440            self.0.negative(),
441        )
442            .into_pyobject(py)
443    }
444}
445
446#[pyclass(name = "RollingGroupOptions", frozen)]
447pub struct PyRollingGroupOptions {
448    inner: RollingGroupOptions,
449}
450
451#[pymethods]
452impl PyRollingGroupOptions {
453    #[getter]
454    fn index_column(&self) -> &str {
455        self.inner.index_column.as_str()
456    }
457
458    #[getter]
459    fn period(&self) -> Wrap<Duration> {
460        Wrap(self.inner.period)
461    }
462
463    #[getter]
464    fn offset(&self) -> Wrap<Duration> {
465        Wrap(self.inner.offset)
466    }
467
468    #[getter]
469    fn closed_window(&self) -> &str {
470        self.inner.closed_window.into()
471    }
472}
473
474#[pyclass(name = "DynamicGroupOptions", frozen)]
475pub struct PyDynamicGroupOptions {
476    inner: DynamicGroupOptions,
477}
478
479#[pymethods]
480impl PyDynamicGroupOptions {
481    #[getter]
482    fn index_column(&self) -> &str {
483        self.inner.index_column.as_str()
484    }
485
486    #[getter]
487    fn every(&self) -> Wrap<Duration> {
488        Wrap(self.inner.every)
489    }
490
491    #[getter]
492    fn period(&self) -> Wrap<Duration> {
493        Wrap(self.inner.period)
494    }
495
496    #[getter]
497    fn offset(&self) -> Wrap<Duration> {
498        Wrap(self.inner.offset)
499    }
500
501    #[getter]
502    fn label(&self) -> &str {
503        self.inner.label.into()
504    }
505
506    #[getter]
507    fn include_boundaries(&self) -> bool {
508        self.inner.include_boundaries
509    }
510
511    #[getter]
512    fn closed_window(&self) -> &str {
513        self.inner.closed_window.into()
514    }
515    #[getter]
516    fn start_by(&self) -> &str {
517        self.inner.start_by.into()
518    }
519}
520
521#[pyclass(name = "GroupbyOptions", frozen)]
522pub struct PyGroupbyOptions {
523    inner: GroupbyOptions,
524}
525
526impl PyGroupbyOptions {
527    pub(crate) fn new(inner: GroupbyOptions) -> Self {
528        Self { inner }
529    }
530}
531
532#[pymethods]
533impl PyGroupbyOptions {
534    #[getter]
535    fn slice(&self) -> Option<(i64, usize)> {
536        self.inner.slice
537    }
538
539    #[getter]
540    fn dynamic(&self) -> Option<PyDynamicGroupOptions> {
541        self.inner
542            .dynamic
543            .as_ref()
544            .map(|f| PyDynamicGroupOptions { inner: f.clone() })
545    }
546
547    #[getter]
548    fn rolling(&self) -> Option<PyRollingGroupOptions> {
549        self.inner
550            .rolling
551            .as_ref()
552            .map(|f| PyRollingGroupOptions { inner: f.clone() })
553    }
554}
555
556pub(crate) fn into_py(py: Python<'_>, expr: &AExpr) -> PyResult<PyObject> {
557    match expr {
558        AExpr::Explode { .. } => Err(PyNotImplementedError::new_err("explode")),
559        AExpr::Column(name) => Column {
560            name: name.into_py_any(py)?,
561        }
562        .into_py_any(py),
563        AExpr::Literal(lit) => {
564            use polars_core::prelude::AnyValue;
565            let dtype: PyObject = Wrap(lit.get_datatype()).into_py_any(py)?;
566            let py_value = match lit {
567                LiteralValue::Dyn(d) => match d {
568                    DynLiteralValue::Int(v) => v.into_py_any(py)?,
569                    DynLiteralValue::Float(v) => v.into_py_any(py)?,
570                    DynLiteralValue::Str(v) => v.into_py_any(py)?,
571                    DynLiteralValue::List(_) => todo!(),
572                },
573                LiteralValue::Scalar(sc) => {
574                    match sc.as_any_value() {
575                        // AnyValue conversion of duration to python's
576                        // datetime.timedelta drops nanoseconds because
577                        // there is no support for them. See
578                        // https://github.com/python/cpython/issues/59648
579                        AnyValue::Duration(delta, _) => delta.into_py_any(py)?,
580                        any => Wrap(any).into_py_any(py)?,
581                    }
582                },
583                LiteralValue::Range(_) => {
584                    return Err(PyNotImplementedError::new_err("range literal"));
585                },
586                LiteralValue::Series(s) => PySeries::new((**s).clone()).into_py_any(py)?,
587            };
588
589            Literal {
590                value: py_value,
591                dtype,
592            }
593        }
594        .into_py_any(py),
595        AExpr::BinaryExpr { left, op, right } => BinaryExpr {
596            left: left.0,
597            op: Wrap(*op).into_py_any(py)?,
598            right: right.0,
599        }
600        .into_py_any(py),
601        AExpr::Cast {
602            expr,
603            dtype,
604            options,
605        } => Cast {
606            expr: expr.0,
607            dtype: Wrap(dtype.clone()).into_py_any(py)?,
608            options: *options as u8,
609        }
610        .into_py_any(py),
611        AExpr::Sort { expr, options } => Sort {
612            expr: expr.0,
613            options: (
614                options.maintain_order,
615                options.nulls_last,
616                options.descending,
617            ),
618        }
619        .into_py_any(py),
620        AExpr::Gather {
621            expr,
622            idx,
623            returns_scalar,
624        } => Gather {
625            expr: expr.0,
626            idx: idx.0,
627            scalar: *returns_scalar,
628        }
629        .into_py_any(py),
630        AExpr::Filter { input, by } => Filter {
631            input: input.0,
632            by: by.0,
633        }
634        .into_py_any(py),
635        AExpr::SortBy {
636            expr,
637            by,
638            sort_options,
639        } => SortBy {
640            expr: expr.0,
641            by: by.iter().map(|n| n.0).collect(),
642            sort_options: (
643                sort_options.maintain_order,
644                sort_options.nulls_last.clone(),
645                sort_options.descending.clone(),
646            ),
647        }
648        .into_py_any(py),
649        AExpr::Agg(aggexpr) => match aggexpr {
650            IRAggExpr::Min {
651                input,
652                propagate_nans,
653            } => Agg {
654                name: "min".into_py_any(py)?,
655                arguments: vec![input.0],
656                options: propagate_nans.into_py_any(py)?,
657            },
658            IRAggExpr::Max {
659                input,
660                propagate_nans,
661            } => Agg {
662                name: "max".into_py_any(py)?,
663                arguments: vec![input.0],
664                options: propagate_nans.into_py_any(py)?,
665            },
666            IRAggExpr::Median(n) => Agg {
667                name: "median".into_py_any(py)?,
668                arguments: vec![n.0],
669                options: py.None(),
670            },
671            IRAggExpr::NUnique(n) => Agg {
672                name: "n_unique".into_py_any(py)?,
673                arguments: vec![n.0],
674                options: py.None(),
675            },
676            IRAggExpr::First(n) => Agg {
677                name: "first".into_py_any(py)?,
678                arguments: vec![n.0],
679                options: py.None(),
680            },
681            IRAggExpr::Last(n) => Agg {
682                name: "last".into_py_any(py)?,
683                arguments: vec![n.0],
684                options: py.None(),
685            },
686            IRAggExpr::Mean(n) => Agg {
687                name: "mean".into_py_any(py)?,
688                arguments: vec![n.0],
689                options: py.None(),
690            },
691            IRAggExpr::Implode(n) => Agg {
692                name: "implode".into_py_any(py)?,
693                arguments: vec![n.0],
694                options: py.None(),
695            },
696            IRAggExpr::Quantile {
697                expr,
698                quantile,
699                method: interpol,
700            } => Agg {
701                name: "quantile".into_py_any(py)?,
702                arguments: vec![expr.0, quantile.0],
703                options: Into::<&str>::into(interpol).into_py_any(py)?,
704            },
705            IRAggExpr::Sum(n) => Agg {
706                name: "sum".into_py_any(py)?,
707                arguments: vec![n.0],
708                options: py.None(),
709            },
710            IRAggExpr::Count {
711                input: n,
712                include_nulls,
713            } => Agg {
714                name: "count".into_py_any(py)?,
715                arguments: vec![n.0],
716                options: include_nulls.into_py_any(py)?,
717            },
718            IRAggExpr::Std(n, ddof) => Agg {
719                name: "std".into_py_any(py)?,
720                arguments: vec![n.0],
721                options: ddof.into_py_any(py)?,
722            },
723            IRAggExpr::Var(n, ddof) => Agg {
724                name: "var".into_py_any(py)?,
725                arguments: vec![n.0],
726                options: ddof.into_py_any(py)?,
727            },
728            IRAggExpr::AggGroups(n) => Agg {
729                name: "agg_groups".into_py_any(py)?,
730                arguments: vec![n.0],
731                options: py.None(),
732            },
733        }
734        .into_py_any(py),
735        AExpr::Ternary {
736            predicate,
737            truthy,
738            falsy,
739        } => Ternary {
740            predicate: predicate.0,
741            truthy: truthy.0,
742            falsy: falsy.0,
743        }
744        .into_py_any(py),
745        AExpr::AnonymousFunction { .. } => Err(PyNotImplementedError::new_err("anonymousfunction")),
746        AExpr::Function {
747            input,
748            function,
749            // TODO: expose options
750            options: _,
751        } => Function {
752            input: input.iter().map(|n| n.node().0).collect(),
753            function_data: match function {
754                IRFunctionExpr::ArrayExpr(_) => {
755                    return Err(PyNotImplementedError::new_err("array expr"));
756                },
757                IRFunctionExpr::BinaryExpr(_) => {
758                    return Err(PyNotImplementedError::new_err("binary expr"));
759                },
760                IRFunctionExpr::Categorical(_) => {
761                    return Err(PyNotImplementedError::new_err("categorical expr"));
762                },
763                IRFunctionExpr::ListExpr(_) => {
764                    return Err(PyNotImplementedError::new_err("list expr"));
765                },
766                IRFunctionExpr::Bitwise(_) => {
767                    return Err(PyNotImplementedError::new_err("bitwise expr"));
768                },
769                IRFunctionExpr::StringExpr(strfun) => match strfun {
770                    IRStringFunction::ConcatHorizontal {
771                        delimiter,
772                        ignore_nulls,
773                    } => (
774                        PyStringFunction::ConcatHorizontal,
775                        delimiter.as_str(),
776                        ignore_nulls,
777                    )
778                        .into_py_any(py),
779                    IRStringFunction::ConcatVertical {
780                        delimiter,
781                        ignore_nulls,
782                    } => (
783                        PyStringFunction::ConcatVertical,
784                        delimiter.as_str(),
785                        ignore_nulls,
786                    )
787                        .into_py_any(py),
788                    #[cfg(feature = "regex")]
789                    IRStringFunction::Contains { literal, strict } => {
790                        (PyStringFunction::Contains, literal, strict).into_py_any(py)
791                    },
792                    IRStringFunction::CountMatches(literal) => {
793                        (PyStringFunction::CountMatches, literal).into_py_any(py)
794                    },
795                    IRStringFunction::EndsWith => (PyStringFunction::EndsWith,).into_py_any(py),
796                    IRStringFunction::Extract(group_index) => {
797                        (PyStringFunction::Extract, group_index).into_py_any(py)
798                    },
799                    IRStringFunction::ExtractAll => (PyStringFunction::ExtractAll,).into_py_any(py),
800                    #[cfg(feature = "extract_groups")]
801                    IRStringFunction::ExtractGroups { dtype, pat } => (
802                        PyStringFunction::ExtractGroups,
803                        &Wrap(dtype.clone()),
804                        pat.as_str(),
805                    )
806                        .into_py_any(py),
807                    #[cfg(feature = "regex")]
808                    IRStringFunction::Find { literal, strict } => {
809                        (PyStringFunction::Find, literal, strict).into_py_any(py)
810                    },
811                    IRStringFunction::ToInteger { dtype: _, strict } => {
812                        (PyStringFunction::ToInteger, strict).into_py_any(py)
813                    },
814                    IRStringFunction::LenBytes => (PyStringFunction::LenBytes,).into_py_any(py),
815                    IRStringFunction::LenChars => (PyStringFunction::LenChars,).into_py_any(py),
816                    IRStringFunction::Lowercase => (PyStringFunction::Lowercase,).into_py_any(py),
817                    #[cfg(feature = "extract_jsonpath")]
818                    IRStringFunction::JsonDecode(_) => {
819                        (PyStringFunction::JsonDecode, <Option<usize>>::None).into_py_any(py)
820                    },
821                    #[cfg(feature = "extract_jsonpath")]
822                    IRStringFunction::JsonPathMatch => {
823                        (PyStringFunction::JsonPathMatch,).into_py_any(py)
824                    },
825                    #[cfg(feature = "regex")]
826                    IRStringFunction::Replace { n, literal } => {
827                        (PyStringFunction::Replace, n, literal).into_py_any(py)
828                    },
829                    #[cfg(feature = "string_normalize")]
830                    IRStringFunction::Normalize { form } => (
831                        PyStringFunction::Normalize,
832                        match form {
833                            UnicodeForm::NFC => "nfc",
834                            UnicodeForm::NFKC => "nfkc",
835                            UnicodeForm::NFD => "nfd",
836                            UnicodeForm::NFKD => "nfkd",
837                        },
838                    )
839                        .into_py_any(py),
840                    IRStringFunction::Reverse => (PyStringFunction::Reverse,).into_py_any(py),
841                    IRStringFunction::PadStart { fill_char } => {
842                        (PyStringFunction::PadStart, fill_char).into_py_any(py)
843                    },
844                    IRStringFunction::PadEnd { fill_char } => {
845                        (PyStringFunction::PadEnd, fill_char).into_py_any(py)
846                    },
847                    IRStringFunction::Slice => (PyStringFunction::Slice,).into_py_any(py),
848                    IRStringFunction::Head => (PyStringFunction::Head,).into_py_any(py),
849                    IRStringFunction::Tail => (PyStringFunction::Tail,).into_py_any(py),
850                    IRStringFunction::HexEncode => (PyStringFunction::HexEncode,).into_py_any(py),
851                    #[cfg(feature = "binary_encoding")]
852                    IRStringFunction::HexDecode(strict) => {
853                        (PyStringFunction::HexDecode, strict).into_py_any(py)
854                    },
855                    IRStringFunction::Base64Encode => {
856                        (PyStringFunction::Base64Encode,).into_py_any(py)
857                    },
858                    #[cfg(feature = "binary_encoding")]
859                    IRStringFunction::Base64Decode(strict) => {
860                        (PyStringFunction::Base64Decode, strict).into_py_any(py)
861                    },
862                    IRStringFunction::StartsWith => (PyStringFunction::StartsWith,).into_py_any(py),
863                    IRStringFunction::StripChars => (PyStringFunction::StripChars,).into_py_any(py),
864                    IRStringFunction::StripCharsStart => {
865                        (PyStringFunction::StripCharsStart,).into_py_any(py)
866                    },
867                    IRStringFunction::StripCharsEnd => {
868                        (PyStringFunction::StripCharsEnd,).into_py_any(py)
869                    },
870                    IRStringFunction::StripPrefix => {
871                        (PyStringFunction::StripPrefix,).into_py_any(py)
872                    },
873                    IRStringFunction::StripSuffix => {
874                        (PyStringFunction::StripSuffix,).into_py_any(py)
875                    },
876                    IRStringFunction::SplitExact { n, inclusive } => {
877                        (PyStringFunction::SplitExact, n, inclusive).into_py_any(py)
878                    },
879                    IRStringFunction::SplitN(n) => (PyStringFunction::SplitN, n).into_py_any(py),
880                    IRStringFunction::Strptime(_, options) => (
881                        PyStringFunction::Strptime,
882                        options.format.as_ref().map(|s| s.as_str()),
883                        options.strict,
884                        options.exact,
885                        options.cache,
886                    )
887                        .into_py_any(py),
888                    IRStringFunction::Split(inclusive) => {
889                        (PyStringFunction::Split, inclusive).into_py_any(py)
890                    },
891                    IRStringFunction::ToDecimal { scale } => {
892                        (PyStringFunction::ToDecimal, scale).into_py_any(py)
893                    },
894                    #[cfg(feature = "nightly")]
895                    IRStringFunction::Titlecase => (PyStringFunction::Titlecase,).into_py_any(py),
896                    IRStringFunction::Uppercase => (PyStringFunction::Uppercase,).into_py_any(py),
897                    IRStringFunction::ZFill => (PyStringFunction::ZFill,).into_py_any(py),
898                    #[cfg(feature = "find_many")]
899                    IRStringFunction::ContainsAny {
900                        ascii_case_insensitive,
901                    } => (PyStringFunction::ContainsAny, ascii_case_insensitive).into_py_any(py),
902                    #[cfg(feature = "find_many")]
903                    IRStringFunction::ReplaceMany {
904                        ascii_case_insensitive,
905                    } => (PyStringFunction::ReplaceMany, ascii_case_insensitive).into_py_any(py),
906                    #[cfg(feature = "find_many")]
907                    IRStringFunction::ExtractMany { .. } => {
908                        return Err(PyNotImplementedError::new_err("extract_many"));
909                    },
910                    #[cfg(feature = "find_many")]
911                    IRStringFunction::FindMany { .. } => {
912                        return Err(PyNotImplementedError::new_err("find_many"));
913                    },
914                    #[cfg(feature = "regex")]
915                    IRStringFunction::EscapeRegex => {
916                        (PyStringFunction::EscapeRegex,).into_py_any(py)
917                    },
918                },
919                IRFunctionExpr::StructExpr(fun) => match fun {
920                    IRStructFunction::FieldByName(name) => {
921                        (PyStructFunction::FieldByName, name.as_str()).into_py_any(py)
922                    },
923                    IRStructFunction::RenameFields(names) => {
924                        (PyStructFunction::RenameFields, names[0].as_str()).into_py_any(py)
925                    },
926                    IRStructFunction::PrefixFields(prefix) => {
927                        (PyStructFunction::PrefixFields, prefix.as_str()).into_py_any(py)
928                    },
929                    IRStructFunction::SuffixFields(prefix) => {
930                        (PyStructFunction::SuffixFields, prefix.as_str()).into_py_any(py)
931                    },
932                    #[cfg(feature = "json")]
933                    IRStructFunction::JsonEncode => (PyStructFunction::JsonEncode,).into_py_any(py),
934                    IRStructFunction::WithFields => {
935                        return Err(PyNotImplementedError::new_err("with_fields"));
936                    },
937                    IRStructFunction::MapFieldNames(_) => {
938                        return Err(PyNotImplementedError::new_err("map_field_names"));
939                    },
940                },
941                IRFunctionExpr::TemporalExpr(fun) => match fun {
942                    IRTemporalFunction::Millennium => {
943                        (PyTemporalFunction::Millennium,).into_py_any(py)
944                    },
945                    IRTemporalFunction::Century => (PyTemporalFunction::Century,).into_py_any(py),
946                    IRTemporalFunction::Year => (PyTemporalFunction::Year,).into_py_any(py),
947                    IRTemporalFunction::IsLeapYear => {
948                        (PyTemporalFunction::IsLeapYear,).into_py_any(py)
949                    },
950                    IRTemporalFunction::IsoYear => (PyTemporalFunction::IsoYear,).into_py_any(py),
951                    IRTemporalFunction::Quarter => (PyTemporalFunction::Quarter,).into_py_any(py),
952                    IRTemporalFunction::Month => (PyTemporalFunction::Month,).into_py_any(py),
953                    IRTemporalFunction::Week => (PyTemporalFunction::Week,).into_py_any(py),
954                    IRTemporalFunction::WeekDay => (PyTemporalFunction::WeekDay,).into_py_any(py),
955                    IRTemporalFunction::Day => (PyTemporalFunction::Day,).into_py_any(py),
956                    IRTemporalFunction::OrdinalDay => {
957                        (PyTemporalFunction::OrdinalDay,).into_py_any(py)
958                    },
959                    IRTemporalFunction::Time => (PyTemporalFunction::Time,).into_py_any(py),
960                    IRTemporalFunction::Date => (PyTemporalFunction::Date,).into_py_any(py),
961                    IRTemporalFunction::Datetime => (PyTemporalFunction::Datetime,).into_py_any(py),
962                    IRTemporalFunction::Duration(time_unit) => {
963                        (PyTemporalFunction::Duration, Wrap(*time_unit)).into_py_any(py)
964                    },
965                    IRTemporalFunction::Hour => (PyTemporalFunction::Hour,).into_py_any(py),
966                    IRTemporalFunction::Minute => (PyTemporalFunction::Minute,).into_py_any(py),
967                    IRTemporalFunction::Second => (PyTemporalFunction::Second,).into_py_any(py),
968                    IRTemporalFunction::Millisecond => {
969                        (PyTemporalFunction::Millisecond,).into_py_any(py)
970                    },
971                    IRTemporalFunction::Microsecond => {
972                        (PyTemporalFunction::Microsecond,).into_py_any(py)
973                    },
974                    IRTemporalFunction::Nanosecond => {
975                        (PyTemporalFunction::Nanosecond,).into_py_any(py)
976                    },
977                    IRTemporalFunction::DaysInMonth => {
978                        (PyTemporalFunction::DaysInMonth,).into_py_any(py)
979                    },
980                    IRTemporalFunction::TotalDays => {
981                        (PyTemporalFunction::TotalDays,).into_py_any(py)
982                    },
983                    IRTemporalFunction::TotalHours => {
984                        (PyTemporalFunction::TotalHours,).into_py_any(py)
985                    },
986                    IRTemporalFunction::TotalMinutes => {
987                        (PyTemporalFunction::TotalMinutes,).into_py_any(py)
988                    },
989                    IRTemporalFunction::TotalSeconds => {
990                        (PyTemporalFunction::TotalSeconds,).into_py_any(py)
991                    },
992                    IRTemporalFunction::TotalMilliseconds => {
993                        (PyTemporalFunction::TotalMilliseconds,).into_py_any(py)
994                    },
995                    IRTemporalFunction::TotalMicroseconds => {
996                        (PyTemporalFunction::TotalMicroseconds,).into_py_any(py)
997                    },
998                    IRTemporalFunction::TotalNanoseconds => {
999                        (PyTemporalFunction::TotalNanoseconds,).into_py_any(py)
1000                    },
1001                    IRTemporalFunction::ToString(format) => {
1002                        (PyTemporalFunction::ToString, format).into_py_any(py)
1003                    },
1004                    IRTemporalFunction::CastTimeUnit(time_unit) => {
1005                        (PyTemporalFunction::CastTimeUnit, Wrap(*time_unit)).into_py_any(py)
1006                    },
1007                    IRTemporalFunction::WithTimeUnit(time_unit) => {
1008                        (PyTemporalFunction::WithTimeUnit, Wrap(*time_unit)).into_py_any(py)
1009                    },
1010                    #[cfg(feature = "timezones")]
1011                    IRTemporalFunction::ConvertTimeZone(time_zone) => {
1012                        (PyTemporalFunction::ConvertTimeZone, time_zone.as_str()).into_py_any(py)
1013                    },
1014                    IRTemporalFunction::TimeStamp(time_unit) => {
1015                        (PyTemporalFunction::TimeStamp, Wrap(*time_unit)).into_py_any(py)
1016                    },
1017                    IRTemporalFunction::Truncate => (PyTemporalFunction::Truncate,).into_py_any(py),
1018                    IRTemporalFunction::OffsetBy => (PyTemporalFunction::OffsetBy,).into_py_any(py),
1019                    IRTemporalFunction::MonthStart => {
1020                        (PyTemporalFunction::MonthStart,).into_py_any(py)
1021                    },
1022                    IRTemporalFunction::MonthEnd => (PyTemporalFunction::MonthEnd,).into_py_any(py),
1023                    #[cfg(feature = "timezones")]
1024                    IRTemporalFunction::BaseUtcOffset => {
1025                        (PyTemporalFunction::BaseUtcOffset,).into_py_any(py)
1026                    },
1027                    #[cfg(feature = "timezones")]
1028                    IRTemporalFunction::DSTOffset => {
1029                        (PyTemporalFunction::DSTOffset,).into_py_any(py)
1030                    },
1031                    IRTemporalFunction::Round => (PyTemporalFunction::Round,).into_py_any(py),
1032                    IRTemporalFunction::Replace => (PyTemporalFunction::Replace).into_py_any(py),
1033                    #[cfg(feature = "timezones")]
1034                    IRTemporalFunction::ReplaceTimeZone(time_zone, non_existent) => (
1035                        PyTemporalFunction::ReplaceTimeZone,
1036                        time_zone.as_ref().map(|s| s.as_str()),
1037                        Into::<&str>::into(non_existent),
1038                    )
1039                        .into_py_any(py),
1040                    IRTemporalFunction::Combine(time_unit) => {
1041                        (PyTemporalFunction::Combine, Wrap(*time_unit)).into_py_any(py)
1042                    },
1043                    IRTemporalFunction::DatetimeFunction {
1044                        time_unit,
1045                        time_zone,
1046                    } => (
1047                        PyTemporalFunction::DatetimeFunction,
1048                        Wrap(*time_unit),
1049                        time_zone.as_ref().map(|s| s.as_str()),
1050                    )
1051                        .into_py_any(py),
1052                },
1053                IRFunctionExpr::Boolean(boolfun) => match boolfun {
1054                    IRBooleanFunction::Any { ignore_nulls } => {
1055                        (PyBooleanFunction::Any, *ignore_nulls).into_py_any(py)
1056                    },
1057                    IRBooleanFunction::All { ignore_nulls } => {
1058                        (PyBooleanFunction::All, *ignore_nulls).into_py_any(py)
1059                    },
1060                    IRBooleanFunction::IsNull => (PyBooleanFunction::IsNull,).into_py_any(py),
1061                    IRBooleanFunction::IsNotNull => (PyBooleanFunction::IsNotNull,).into_py_any(py),
1062                    IRBooleanFunction::IsFinite => (PyBooleanFunction::IsFinite,).into_py_any(py),
1063                    IRBooleanFunction::IsInfinite => {
1064                        (PyBooleanFunction::IsInfinite,).into_py_any(py)
1065                    },
1066                    IRBooleanFunction::IsNan => (PyBooleanFunction::IsNan,).into_py_any(py),
1067                    IRBooleanFunction::IsNotNan => (PyBooleanFunction::IsNotNan,).into_py_any(py),
1068                    IRBooleanFunction::IsFirstDistinct => {
1069                        (PyBooleanFunction::IsFirstDistinct,).into_py_any(py)
1070                    },
1071                    IRBooleanFunction::IsLastDistinct => {
1072                        (PyBooleanFunction::IsLastDistinct,).into_py_any(py)
1073                    },
1074                    IRBooleanFunction::IsUnique => (PyBooleanFunction::IsUnique,).into_py_any(py),
1075                    IRBooleanFunction::IsDuplicated => {
1076                        (PyBooleanFunction::IsDuplicated,).into_py_any(py)
1077                    },
1078                    IRBooleanFunction::IsBetween { closed } => {
1079                        (PyBooleanFunction::IsBetween, Into::<&str>::into(closed)).into_py_any(py)
1080                    },
1081                    #[cfg(feature = "is_in")]
1082                    IRBooleanFunction::IsIn { nulls_equal } => {
1083                        (PyBooleanFunction::IsIn, nulls_equal).into_py_any(py)
1084                    },
1085                    IRBooleanFunction::IsClose {
1086                        abs_tol,
1087                        rel_tol,
1088                        nans_equal,
1089                    } => (PyBooleanFunction::IsClose, abs_tol.0, rel_tol.0, nans_equal)
1090                        .into_py_any(py),
1091                    IRBooleanFunction::AllHorizontal => {
1092                        (PyBooleanFunction::AllHorizontal,).into_py_any(py)
1093                    },
1094                    IRBooleanFunction::AnyHorizontal => {
1095                        (PyBooleanFunction::AnyHorizontal,).into_py_any(py)
1096                    },
1097                    IRBooleanFunction::Not => (PyBooleanFunction::Not,).into_py_any(py),
1098                },
1099                IRFunctionExpr::Abs => ("abs",).into_py_any(py),
1100                #[cfg(feature = "hist")]
1101                IRFunctionExpr::Hist {
1102                    bin_count,
1103                    include_category,
1104                    include_breakpoint,
1105                } => ("hist", bin_count, include_category, include_breakpoint).into_py_any(py),
1106                IRFunctionExpr::NullCount => ("null_count",).into_py_any(py),
1107                IRFunctionExpr::Pow(f) => match f {
1108                    IRPowFunction::Generic => ("pow",).into_py_any(py),
1109                    IRPowFunction::Sqrt => ("sqrt",).into_py_any(py),
1110                    IRPowFunction::Cbrt => ("cbrt",).into_py_any(py),
1111                },
1112                IRFunctionExpr::Hash(seed, seed_1, seed_2, seed_3) => {
1113                    ("hash", seed, seed_1, seed_2, seed_3).into_py_any(py)
1114                },
1115                IRFunctionExpr::ArgWhere => ("argwhere",).into_py_any(py),
1116                #[cfg(feature = "index_of")]
1117                IRFunctionExpr::IndexOf => ("index_of",).into_py_any(py),
1118                #[cfg(feature = "search_sorted")]
1119                IRFunctionExpr::SearchSorted { side, descending } => (
1120                    "search_sorted",
1121                    match side {
1122                        SearchSortedSide::Any => "any",
1123                        SearchSortedSide::Left => "left",
1124                        SearchSortedSide::Right => "right",
1125                    },
1126                    descending,
1127                )
1128                    .into_py_any(py),
1129                IRFunctionExpr::Range(_) => return Err(PyNotImplementedError::new_err("range")),
1130                #[cfg(feature = "trigonometry")]
1131                IRFunctionExpr::Trigonometry(trigfun) => {
1132                    use polars_plan::plans::IRTrigonometricFunction;
1133
1134                    match trigfun {
1135                        IRTrigonometricFunction::Cos => ("cos",),
1136                        IRTrigonometricFunction::Cot => ("cot",),
1137                        IRTrigonometricFunction::Sin => ("sin",),
1138                        IRTrigonometricFunction::Tan => ("tan",),
1139                        IRTrigonometricFunction::ArcCos => ("arccos",),
1140                        IRTrigonometricFunction::ArcSin => ("arcsin",),
1141                        IRTrigonometricFunction::ArcTan => ("arctan",),
1142                        IRTrigonometricFunction::Cosh => ("cosh",),
1143                        IRTrigonometricFunction::Sinh => ("sinh",),
1144                        IRTrigonometricFunction::Tanh => ("tanh",),
1145                        IRTrigonometricFunction::ArcCosh => ("arccosh",),
1146                        IRTrigonometricFunction::ArcSinh => ("arcsinh",),
1147                        IRTrigonometricFunction::ArcTanh => ("arctanh",),
1148                        IRTrigonometricFunction::Degrees => ("degrees",),
1149                        IRTrigonometricFunction::Radians => ("radians",),
1150                    }
1151                    .into_py_any(py)
1152                },
1153                #[cfg(feature = "trigonometry")]
1154                IRFunctionExpr::Atan2 => ("atan2",).into_py_any(py),
1155                #[cfg(feature = "sign")]
1156                IRFunctionExpr::Sign => ("sign",).into_py_any(py),
1157                IRFunctionExpr::FillNull => ("fill_null",).into_py_any(py),
1158                IRFunctionExpr::RollingExpr { function, .. } => {
1159                    return Err(PyNotImplementedError::new_err(format!("{function}")));
1160                },
1161                IRFunctionExpr::RollingExprBy { function_by, .. } => match function_by {
1162                    IRRollingFunctionBy::MinBy => {
1163                        return Err(PyNotImplementedError::new_err("rolling min by"));
1164                    },
1165                    IRRollingFunctionBy::MaxBy => {
1166                        return Err(PyNotImplementedError::new_err("rolling max by"));
1167                    },
1168                    IRRollingFunctionBy::MeanBy => {
1169                        return Err(PyNotImplementedError::new_err("rolling mean by"));
1170                    },
1171                    IRRollingFunctionBy::SumBy => {
1172                        return Err(PyNotImplementedError::new_err("rolling sum by"));
1173                    },
1174                    IRRollingFunctionBy::QuantileBy => {
1175                        return Err(PyNotImplementedError::new_err("rolling quantile by"));
1176                    },
1177                    IRRollingFunctionBy::VarBy => {
1178                        return Err(PyNotImplementedError::new_err("rolling var by"));
1179                    },
1180                    IRRollingFunctionBy::StdBy => {
1181                        return Err(PyNotImplementedError::new_err("rolling std by"));
1182                    },
1183                },
1184                IRFunctionExpr::Rechunk => ("rechunk",).into_py_any(py),
1185                IRFunctionExpr::Append { upcast } => ("append", upcast).into_py_any(py),
1186                IRFunctionExpr::ShiftAndFill => ("shift_and_fill",).into_py_any(py),
1187                IRFunctionExpr::Shift => ("shift",).into_py_any(py),
1188                IRFunctionExpr::DropNans => ("drop_nans",).into_py_any(py),
1189                IRFunctionExpr::DropNulls => ("drop_nulls",).into_py_any(py),
1190                IRFunctionExpr::Mode => ("mode",).into_py_any(py),
1191                IRFunctionExpr::Skew(bias) => ("skew", bias).into_py_any(py),
1192                IRFunctionExpr::Kurtosis(fisher, bias) => {
1193                    ("kurtosis", fisher, bias).into_py_any(py)
1194                },
1195                IRFunctionExpr::Reshape(_) => {
1196                    return Err(PyNotImplementedError::new_err("reshape"));
1197                },
1198                #[cfg(feature = "repeat_by")]
1199                IRFunctionExpr::RepeatBy => ("repeat_by",).into_py_any(py),
1200                IRFunctionExpr::ArgUnique => ("arg_unique",).into_py_any(py),
1201                IRFunctionExpr::ArgMin => ("arg_min",).into_py_any(py),
1202                IRFunctionExpr::ArgMax => ("arg_max",).into_py_any(py),
1203                IRFunctionExpr::ArgSort {
1204                    descending,
1205                    nulls_last,
1206                } => ("arg_max", descending, nulls_last).into_py_any(py),
1207                IRFunctionExpr::Product => ("product",).into_py_any(py),
1208                IRFunctionExpr::Repeat => ("repeat",).into_py_any(py),
1209                IRFunctionExpr::Rank { options, seed } => {
1210                    let method = match options.method {
1211                        RankMethod::Average => "average",
1212                        RankMethod::Min => "min",
1213                        RankMethod::Max => "max",
1214                        RankMethod::Dense => "dense",
1215                        RankMethod::Ordinal => "ordinal",
1216                        RankMethod::Random => "random",
1217                    };
1218                    ("rank", method, options.descending, seed.map(|s| s as i64)).into_py_any(py)
1219                },
1220                IRFunctionExpr::Clip { has_min, has_max } => {
1221                    ("clip", has_min, has_max).into_py_any(py)
1222                },
1223                IRFunctionExpr::AsStruct => ("as_struct",).into_py_any(py),
1224                #[cfg(feature = "top_k")]
1225                IRFunctionExpr::TopK { descending } => ("top_k", descending).into_py_any(py),
1226                IRFunctionExpr::CumCount { reverse } => ("cum_count", reverse).into_py_any(py),
1227                IRFunctionExpr::CumSum { reverse } => ("cum_sum", reverse).into_py_any(py),
1228                IRFunctionExpr::CumProd { reverse } => ("cum_prod", reverse).into_py_any(py),
1229                IRFunctionExpr::CumMin { reverse } => ("cum_min", reverse).into_py_any(py),
1230                IRFunctionExpr::CumMax { reverse } => ("cum_max", reverse).into_py_any(py),
1231                IRFunctionExpr::Reverse => ("reverse",).into_py_any(py),
1232                IRFunctionExpr::ValueCounts {
1233                    sort,
1234                    parallel,
1235                    name,
1236                    normalize,
1237                } => ("value_counts", sort, parallel, name.as_str(), normalize).into_py_any(py),
1238                IRFunctionExpr::UniqueCounts => ("unique_counts",).into_py_any(py),
1239                IRFunctionExpr::ApproxNUnique => ("approx_n_unique",).into_py_any(py),
1240                IRFunctionExpr::Coalesce => ("coalesce",).into_py_any(py),
1241                IRFunctionExpr::Diff(null_behaviour) => (
1242                    "diff",
1243                    match null_behaviour {
1244                        NullBehavior::Drop => "drop",
1245                        NullBehavior::Ignore => "ignore",
1246                    },
1247                )
1248                    .into_py_any(py),
1249                #[cfg(feature = "pct_change")]
1250                IRFunctionExpr::PctChange => ("pct_change",).into_py_any(py),
1251                IRFunctionExpr::Interpolate(method) => (
1252                    "interpolate",
1253                    match method {
1254                        InterpolationMethod::Linear => "linear",
1255                        InterpolationMethod::Nearest => "nearest",
1256                    },
1257                )
1258                    .into_py_any(py),
1259                IRFunctionExpr::InterpolateBy => ("interpolate_by",).into_py_any(py),
1260                IRFunctionExpr::Entropy { base, normalize } => {
1261                    ("entropy", base, normalize).into_py_any(py)
1262                },
1263                IRFunctionExpr::Log => ("log",).into_py_any(py),
1264                IRFunctionExpr::Log1p => ("log1p",).into_py_any(py),
1265                IRFunctionExpr::Exp => ("exp",).into_py_any(py),
1266                IRFunctionExpr::Unique(maintain_order) => {
1267                    ("unique", maintain_order).into_py_any(py)
1268                },
1269                IRFunctionExpr::Round { decimals, mode } => {
1270                    ("round", decimals, Into::<&str>::into(mode)).into_py_any(py)
1271                },
1272                IRFunctionExpr::RoundSF { digits } => ("round_sig_figs", digits).into_py_any(py),
1273                IRFunctionExpr::Floor => ("floor",).into_py_any(py),
1274                IRFunctionExpr::Ceil => ("ceil",).into_py_any(py),
1275                IRFunctionExpr::UpperBound => ("upper_bound",).into_py_any(py),
1276                IRFunctionExpr::LowerBound => ("lower_bound",).into_py_any(py),
1277                IRFunctionExpr::Fused(_) => return Err(PyNotImplementedError::new_err("fused")),
1278                IRFunctionExpr::ConcatExpr(_) => {
1279                    return Err(PyNotImplementedError::new_err("concat expr"));
1280                },
1281                IRFunctionExpr::Correlation { .. } => {
1282                    return Err(PyNotImplementedError::new_err("corr"));
1283                },
1284                #[cfg(feature = "peaks")]
1285                IRFunctionExpr::PeakMin => ("peak_max",).into_py_any(py),
1286                #[cfg(feature = "peaks")]
1287                IRFunctionExpr::PeakMax => ("peak_min",).into_py_any(py),
1288                #[cfg(feature = "cutqcut")]
1289                IRFunctionExpr::Cut { .. } => return Err(PyNotImplementedError::new_err("cut")),
1290                #[cfg(feature = "cutqcut")]
1291                IRFunctionExpr::QCut { .. } => return Err(PyNotImplementedError::new_err("qcut")),
1292                #[cfg(feature = "rle")]
1293                IRFunctionExpr::RLE => ("rle",).into_py_any(py),
1294                #[cfg(feature = "rle")]
1295                IRFunctionExpr::RLEID => ("rle_id",).into_py_any(py),
1296                IRFunctionExpr::ToPhysical => ("to_physical",).into_py_any(py),
1297                IRFunctionExpr::Random { .. } => {
1298                    return Err(PyNotImplementedError::new_err("random"));
1299                },
1300                IRFunctionExpr::SetSortedFlag(sorted) => (
1301                    "set_sorted",
1302                    match sorted {
1303                        IsSorted::Ascending => "ascending",
1304                        IsSorted::Descending => "descending",
1305                        IsSorted::Not => "not",
1306                    },
1307                )
1308                    .into_py_any(py),
1309                #[cfg(feature = "ffi_plugin")]
1310                IRFunctionExpr::FfiPlugin { .. } => {
1311                    return Err(PyNotImplementedError::new_err("ffi plugin"));
1312                },
1313                IRFunctionExpr::FoldHorizontal { .. } => {
1314                    Err(PyNotImplementedError::new_err("fold"))
1315                },
1316                IRFunctionExpr::ReduceHorizontal { .. } => {
1317                    Err(PyNotImplementedError::new_err("reduce"))
1318                },
1319                IRFunctionExpr::CumReduceHorizontal { .. } => {
1320                    Err(PyNotImplementedError::new_err("cum_reduce"))
1321                },
1322                IRFunctionExpr::CumFoldHorizontal { .. } => {
1323                    Err(PyNotImplementedError::new_err("cum_fold"))
1324                },
1325                IRFunctionExpr::SumHorizontal { ignore_nulls } => {
1326                    ("sum_horizontal", ignore_nulls).into_py_any(py)
1327                },
1328                IRFunctionExpr::MaxHorizontal => ("max_horizontal",).into_py_any(py),
1329                IRFunctionExpr::MeanHorizontal { ignore_nulls } => {
1330                    ("mean_horizontal", ignore_nulls).into_py_any(py)
1331                },
1332                IRFunctionExpr::MinHorizontal => ("min_horizontal",).into_py_any(py),
1333                IRFunctionExpr::EwmMean { options: _ } => {
1334                    return Err(PyNotImplementedError::new_err("ewm mean"));
1335                },
1336                IRFunctionExpr::EwmStd { options: _ } => {
1337                    return Err(PyNotImplementedError::new_err("ewm std"));
1338                },
1339                IRFunctionExpr::EwmVar { options: _ } => {
1340                    return Err(PyNotImplementedError::new_err("ewm var"));
1341                },
1342                IRFunctionExpr::Replace => ("replace",).into_py_any(py),
1343                IRFunctionExpr::ReplaceStrict { return_dtype: _ } => {
1344                    // Can ignore the return dtype because it is encoded in the schema.
1345                    ("replace_strict",).into_py_any(py)
1346                },
1347                IRFunctionExpr::Negate => ("negate",).into_py_any(py),
1348                IRFunctionExpr::FillNullWithStrategy(strategy) => {
1349                    let (strategy_str, py_limit): (&str, PyObject) = match strategy {
1350                        FillNullStrategy::Forward(limit) => {
1351                            let py_limit = limit
1352                                .map(|v| PyInt::new(py, v).into())
1353                                .unwrap_or_else(|| py.None());
1354                            ("forward", py_limit)
1355                        },
1356                        FillNullStrategy::Backward(limit) => {
1357                            let py_limit = limit
1358                                .map(|v| PyInt::new(py, v).into())
1359                                .unwrap_or_else(|| py.None());
1360                            ("backward", py_limit)
1361                        },
1362                        FillNullStrategy::Min => ("min", py.None()),
1363                        FillNullStrategy::Max => ("max", py.None()),
1364                        FillNullStrategy::Mean => ("mean", py.None()),
1365                        FillNullStrategy::Zero => ("zero", py.None()),
1366                        FillNullStrategy::One => ("one", py.None()),
1367                    };
1368
1369                    ("fill_null_with_strategy", strategy_str, py_limit).into_py_any(py)
1370                },
1371                IRFunctionExpr::GatherEvery { n, offset } => {
1372                    ("gather_every", offset, n).into_py_any(py)
1373                },
1374                IRFunctionExpr::Reinterpret(signed) => ("reinterpret", signed).into_py_any(py),
1375                IRFunctionExpr::ExtendConstant => ("extend_constant",).into_py_any(py),
1376                IRFunctionExpr::Business(_) => {
1377                    return Err(PyNotImplementedError::new_err("business"));
1378                },
1379                #[cfg(feature = "top_k")]
1380                IRFunctionExpr::TopKBy { descending } => ("top_k_by", descending).into_py_any(py),
1381                IRFunctionExpr::EwmMeanBy { half_life: _ } => {
1382                    return Err(PyNotImplementedError::new_err("ewm_mean_by"));
1383                },
1384                IRFunctionExpr::RowEncode(..) => {
1385                    return Err(PyNotImplementedError::new_err("row_encode"));
1386                },
1387                IRFunctionExpr::RowDecode(..) => {
1388                    return Err(PyNotImplementedError::new_err("row_decode"));
1389                },
1390            }?,
1391            options: py.None(),
1392        }
1393        .into_py_any(py),
1394        AExpr::Window {
1395            function,
1396            partition_by,
1397            order_by,
1398            options,
1399        } => {
1400            let function = function.0;
1401            let partition_by = partition_by.iter().map(|n| n.0).collect();
1402            let order_by_descending = order_by
1403                .map(|(_, options)| options.descending)
1404                .unwrap_or(false);
1405            let order_by_nulls_last = order_by
1406                .map(|(_, options)| options.nulls_last)
1407                .unwrap_or(false);
1408            let order_by = order_by.map(|(n, _)| n.0);
1409
1410            let options = match options {
1411                WindowType::Over(options) => PyWindowMapping { inner: *options }.into_py_any(py)?,
1412                WindowType::Rolling(options) => PyRollingGroupOptions {
1413                    inner: options.clone(),
1414                }
1415                .into_py_any(py)?,
1416            };
1417            Window {
1418                function,
1419                partition_by,
1420                order_by,
1421                order_by_descending,
1422                order_by_nulls_last,
1423                options,
1424            }
1425            .into_py_any(py)
1426        },
1427        AExpr::Slice {
1428            input,
1429            offset,
1430            length,
1431        } => Slice {
1432            input: input.0,
1433            offset: offset.0,
1434            length: length.0,
1435        }
1436        .into_py_any(py),
1437        AExpr::Len => Len {}.into_py_any(py),
1438        AExpr::Eval { .. } => Err(PyNotImplementedError::new_err("list.eval")),
1439    }
1440}