polars_python/lazyframe/visitor/
expr_nodes.rs

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