Skip to main content

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