polars_python/lazyframe/visitor/
expr_nodes.rs

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