polars_python/lazyframe/visitor/
expr_nodes.rs

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