trs_dataframe/dataframe/
python.rs

1use std::collections::HashMap;
2
3use crate::{DataFrame, DataValue, JoinRelation, Key};
4use data_value::Extract as _;
5use ndarray::Array1;
6use numpy::PyArray2;
7use pyo3::{exceptions::PyTypeError, prelude::*, types::PyList};
8use tracing::trace;
9
10impl DataFrame {
11    fn select_data(
12        &self,
13        keys: Option<Vec<String>>,
14        transposed: Option<bool>,
15    ) -> Result<ndarray::Array2<DataValue>, crate::error::Error> {
16        let keys = keys
17            .unwrap_or(self.keys())
18            .into_iter()
19            .map(Key::from)
20            .collect::<Vec<Key>>();
21        if transposed.unwrap_or(false) {
22            self.select(Some(keys.as_slice()))
23        } else {
24            self.select_transposed(Some(keys.as_slice()))
25        }
26    }
27}
28
29enum DfOrDict {
30    DataFrame(DataFrame),
31    Dict(HashMap<String, DataValue>),
32}
33
34impl DfOrDict {
35    pub fn new(object: Bound<'_, PyAny>) -> Result<DfOrDict, PyErr> {
36        if let Ok(df) = object.extract::<DataFrame>() {
37            Ok(DfOrDict::DataFrame(df))
38        } else {
39            let dict: HashMap<String, DataValue> = object.extract()?;
40            Ok(DfOrDict::Dict(dict))
41        }
42    }
43}
44
45#[pymethods]
46impl DataFrame {
47    #[new]
48    pub fn init() -> Self {
49        Self::default()
50    }
51
52    #[cfg(feature = "polars-df")]
53    #[staticmethod]
54    pub fn from_polars(df: pyo3_polars::PyDataFrame) -> Self {
55        df.0.into()
56    }
57
58    #[staticmethod]
59    pub fn from_dict_np(df: HashMap<String, Vec<DataValue>>) -> Self {
60        let mut result_df: Vec<(Key, Vec<DataValue>)> = Vec::new();
61        for (key, value) in df.into_iter() {
62            result_df.push((key.as_str().into(), value));
63        }
64        result_df.into()
65    }
66
67    pub fn keys(&self) -> Vec<String> {
68        self.dataframe
69            .keys()
70            .iter()
71            .map(|x| x.name().to_string())
72            .collect()
73    }
74
75    #[cfg(feature = "polars-df")]
76    #[pyo3(name = "as_polars")]
77    pub fn py_as_polars(&self) -> PyResult<pyo3_polars::PyDataFrame> {
78        let df = self
79            .as_polars()
80            .map_err(|e| PyErr::new::<PyTypeError, _>(format!("Cannot prepare polars DF: {e}")))?;
81        Ok(pyo3_polars::PyDataFrame(df))
82    }
83
84    pub fn apply(&mut self, function: Bound<'_, PyAny>) -> Result<(), PyErr> {
85        let df: DataFrame = pyo3::Python::with_gil(|py| {
86            let self_ = self
87                .clone()
88                .into_pyobject(py)
89                .expect("BUG: cannot convert to PyObject");
90            let result = function.call1((self_,)).expect("BUG: cannot call function");
91            result
92                .extract::<Bound<DataFrame>>()
93                .expect("BUG: cannot extract data frame")
94                .unbind()
95                .extract(py)
96                .expect("BUG: cannot extract data frame")
97        });
98        self.dataframe = df.dataframe;
99        Ok(())
100    }
101
102    #[pyo3(signature = (keys=None, transposed=None))]
103    pub fn as_numpy_u32<'py>(
104        &self,
105        keys: Option<Vec<String>>,
106        transposed: Option<bool>,
107        py: Python<'py>,
108    ) -> PyResult<Bound<'py, numpy::PyArray2<u32>>> {
109        let data = self
110            .select_data(keys, transposed)
111            .map_err(|e| PyErr::new::<PyTypeError, _>(format!("Cannot select data: {e}")))?;
112        Ok(PyArray2::from_array(py, &data.mapv(|x| u32::extract(&x))))
113    }
114
115    #[pyo3(signature = (keys=None, transposed=None))]
116    pub fn as_numpy_u64<'py>(
117        &self,
118        keys: Option<Vec<String>>,
119        transposed: Option<bool>,
120        py: Python<'py>,
121    ) -> PyResult<Bound<'py, numpy::PyArray2<u64>>> {
122        let data = self
123            .select_data(keys, transposed)
124            .map_err(|e| PyErr::new::<PyTypeError, _>(format!("Cannot select data: {e}")))?;
125        Ok(PyArray2::from_array(py, &data.mapv(|x| u64::extract(&x))))
126    }
127
128    #[pyo3(signature = (keys=None, transposed=None))]
129    pub fn as_numpy_i32<'py>(
130        &self,
131        keys: Option<Vec<String>>,
132        transposed: Option<bool>,
133        py: Python<'py>,
134    ) -> PyResult<Bound<'py, numpy::PyArray2<i32>>> {
135        let data = self
136            .select_data(keys, transposed)
137            .map_err(|e| PyErr::new::<PyTypeError, _>(format!("Cannot select data: {e}")))?;
138        Ok(PyArray2::from_array(py, &data.mapv(|x| i32::extract(&x))))
139    }
140
141    #[pyo3(signature = (keys=None, transposed=None))]
142    pub fn as_numpy_i64<'py>(
143        &self,
144        keys: Option<Vec<String>>,
145        transposed: Option<bool>,
146        py: Python<'py>,
147    ) -> PyResult<Bound<'py, numpy::PyArray2<i64>>> {
148        let data = self
149            .select_data(keys, transposed)
150            .map_err(|e| PyErr::new::<PyTypeError, _>(format!("Cannot select data: {e}")))?;
151        Ok(PyArray2::from_array(py, &data.mapv(|x| i64::extract(&x))))
152    }
153
154    #[pyo3(signature = (keys=None, transposed=None))]
155    pub fn as_numpy_f32<'py>(
156        &self,
157        keys: Option<Vec<String>>,
158        transposed: Option<bool>,
159        py: Python<'py>,
160    ) -> PyResult<Bound<'py, numpy::PyArray2<f32>>> {
161        let data = self
162            .select_data(keys, transposed)
163            .map_err(|e| PyErr::new::<PyTypeError, _>(format!("Cannot select data: {e}")))?;
164        Ok(PyArray2::from_array(py, &data.mapv(|x| f32::extract(&x))))
165    }
166
167    #[pyo3(signature = (keys=None, transposed=None))]
168    pub fn as_numpy_f64<'py>(
169        &self,
170        keys: Option<Vec<String>>,
171        transposed: Option<bool>,
172        py: Python<'py>,
173    ) -> PyResult<Bound<'py, numpy::PyArray2<f64>>> {
174        let data = self
175            .select_data(keys, transposed)
176            .map_err(|e| PyErr::new::<PyTypeError, _>(format!("Cannot select data: {e}")))?;
177        Ok(PyArray2::from_array(py, &data.mapv(|x| f64::extract(&x))))
178    }
179
180    #[pyo3(name = "shrink")]
181    pub fn py_shrink(&mut self) {
182        self.dataframe.shrink();
183    }
184
185    #[pyo3(name = "add_metadata")]
186    pub fn py_add_metadata(&mut self, key: String, value: DataValue) {
187        self.metadata.insert(key, value);
188    }
189
190    #[pyo3(name = "get_metadata")]
191    pub fn py_get_metadata(&self, key: &str) -> Option<DataValue> {
192        self.metadata.get(key).cloned()
193    }
194
195    #[pyo3(name = "rename_key")]
196    pub fn py_rename_key(&mut self, key: &str, new_name: &str) -> Result<(), PyErr> {
197        // fixme this may have a problem when the type is different and checked
198        self.dataframe
199            .rename_key(key, new_name.into())
200            .map_err(|e| PyErr::new::<PyTypeError, _>(format!("{e}")))
201    }
202
203    #[pyo3(name = "add_alias")]
204    pub fn py_add_alias(&mut self, key: &str, new_name: &str) -> Result<(), PyErr> {
205        self.dataframe
206            .add_alias(key, new_name)
207            .map_err(|e| PyErr::new::<PyTypeError, _>(format!("{e}")))
208    }
209
210    #[pyo3(name = "select", signature = (keys=None))]
211    pub fn py_select<'py>(
212        &self,
213        py: Python<'py>,
214        keys: Option<Vec<String>>,
215    ) -> Result<Bound<'py, PyList>, PyErr> {
216        let keys = keys
217            .unwrap_or(self.keys())
218            .into_iter()
219            .map(Key::from)
220            .collect::<Vec<Key>>();
221        let selected = self
222            .select(Some(keys.as_slice()))
223            .map_err(|e| PyErr::new::<PyTypeError, _>(format!("Cannot select data: {e}")))?;
224
225        let list = PyList::empty(py);
226        for rows in selected.rows() {
227            let row = PyList::empty(py);
228            for value in rows.iter() {
229                row.append(value.clone())
230                    .expect("BUG: cannot append to list");
231            }
232            list.append(row).expect("BUG: cannot append to list");
233        }
234        Ok(list)
235    }
236
237    #[pyo3(name = "select_transposed", signature = (keys=None))]
238    pub fn py_select_transposed<'py>(
239        &self,
240        py: Python<'py>,
241        keys: Option<Vec<String>>,
242    ) -> Result<Bound<'py, PyList>, PyErr> {
243        let keys = keys
244            .unwrap_or(self.keys())
245            .into_iter()
246            .map(Key::from)
247            .collect::<Vec<Key>>();
248        let selected = self
249            .select_transposed(Some(keys.as_slice()))
250            .map_err(|e| PyErr::new::<PyTypeError, _>(format!("Cannot select data: {e}")))?;
251
252        let list = PyList::empty(py);
253        for rows in selected.rows() {
254            let row = PyList::empty(py);
255            for value in rows.iter() {
256                row.append(value.clone())?;
257            }
258            list.append(row)?;
259        }
260        Ok(list)
261    }
262
263    #[pyo3(name = "select_column")]
264    pub fn py_select_column<'py>(
265        &self,
266        py: Python<'py>,
267        key: String,
268    ) -> Result<Bound<'py, PyList>, PyErr> {
269        let selected = self
270            .select_column(Key::from(key))
271            .ok_or_else(|| PyErr::new::<PyTypeError, _>("Cannot select column"))?;
272
273        let list = PyList::empty(py);
274        for x in selected.to_vec().into_iter() {
275            list.append(x)?;
276        }
277
278        Ok(list)
279    }
280
281    #[pyo3(name = "join")]
282    pub fn py_join(&mut self, other: DataFrame, join_type: JoinRelation) -> Result<(), PyErr> {
283        self.dataframe
284            .join(other.dataframe, &join_type)
285            .map_err(|e| PyErr::new::<PyTypeError, _>(format!("Cannot join data: {e}")))?;
286
287        Ok(())
288    }
289
290    #[pyo3(name = "push")]
291    pub fn py_push(&mut self, data: HashMap<Key, DataValue>) -> Result<(), PyErr> {
292        self.dataframe
293            .push(data)
294            .map_err(|e| PyErr::new::<PyTypeError, _>(format!("Cannot join data: {e}")))?;
295        Ok(())
296    }
297
298    #[pyo3(name = "add_column")]
299    pub fn py_add_column(&mut self, key: Key, data: Vec<DataValue>) -> Result<(), PyErr> {
300        self.dataframe
301            .add_single_column(key, Array1::from_vec(data))
302            .map_err(|e| PyErr::new::<PyTypeError, _>(format!("Cannot join data: {e}")))?;
303        Ok(())
304    }
305
306    pub fn add_constant(&mut self, key: Key, feature: DataValue) -> Result<(), PyErr> {
307        self.constants.insert(key, feature);
308        Ok(())
309    }
310
311    fn __repr__(&self) -> String {
312        self.to_string()
313    }
314
315    fn __str__(&self) -> String {
316        self.to_string()
317    }
318
319    pub fn __iadd__(&mut self, object: Bound<'_, PyAny>) -> Result<(), PyErr> {
320        trace!("{object:?}");
321        let df_or_dict = DfOrDict::new(object)?;
322        match df_or_dict {
323            DfOrDict::DataFrame(df) => {
324                self.dataframe += df.dataframe;
325            }
326            DfOrDict::Dict(dict) => {
327                self.dataframe += dict;
328            }
329        }
330        Ok(())
331    }
332
333    pub fn __isub__(&mut self, object: Bound<'_, PyAny>) -> Result<(), PyErr> {
334        trace!("{object:?}");
335
336        let df_or_dict = DfOrDict::new(object)?;
337        match df_or_dict {
338            DfOrDict::DataFrame(df) => {
339                self.dataframe -= df.dataframe;
340            }
341            DfOrDict::Dict(dict) => {
342                self.dataframe -= dict;
343            }
344        }
345        Ok(())
346    }
347
348    pub fn __imul__(&mut self, object: Bound<'_, PyAny>) -> Result<(), PyErr> {
349        trace!("{object:?}");
350        let df_or_dict = DfOrDict::new(object)?;
351        match df_or_dict {
352            DfOrDict::DataFrame(df) => {
353                self.dataframe *= df.dataframe;
354            }
355            DfOrDict::Dict(dict) => {
356                self.dataframe *= dict;
357            }
358        }
359        Ok(())
360    }
361
362    pub fn __itruediv__(&mut self, object: Bound<'_, PyAny>) -> Result<(), PyErr> {
363        trace!("{object:?}");
364        let df_or_dict = DfOrDict::new(object)?;
365        match df_or_dict {
366            DfOrDict::DataFrame(df) => {
367                self.dataframe /= df.dataframe;
368            }
369            DfOrDict::Dict(dict) => {
370                self.dataframe /= dict;
371            }
372        }
373        Ok(())
374    }
375
376    pub fn __len__(&mut self) -> Result<usize, PyErr> {
377        Ok(self.dataframe.len())
378    }
379}
380
381#[cfg(test)]
382mod test {
383
384    use super::*;
385    use data_value::{stdhashmap, DataValue};
386    use halfbrown::hashmap;
387    use pyo3::ffi::c_str;
388    use rstest::*;
389    use tracing_test::traced_test;
390
391    #[fixture]
392    fn df() -> DataFrame {
393        let mut df = DataFrame::init();
394        assert!(df
395            .push(hashmap! {
396                Key::from("key1") => DataValue::U32(1),
397                Key::from("key2") => DataValue::U32(2),
398            })
399            .is_ok());
400        assert!(df
401            .push(hashmap! {
402                Key::from("key1") => DataValue::U32(11),
403                Key::from("key2") => DataValue::U32(21),
404            })
405            .is_ok());
406        df
407    }
408
409    #[fixture]
410    fn hm() -> HashMap<String, DataValue> {
411        stdhashmap!(
412            "key1".to_string() => DataValue::U32(2),
413            "key2".to_string() => DataValue::U32(3),
414        )
415    }
416
417    #[rstest]
418    fn test_select_data(df: DataFrame) {
419        let data = df.select_data(Some(vec!["key1".into(), "key2".into()]), Some(false));
420        assert!(data.is_ok());
421        assert_eq!(
422            data.unwrap(),
423            ndarray::array![[1u32.into(), 11u32.into()], [2u32.into(), 21u32.into()]]
424        );
425
426        let data = df.select_data(Some(vec!["key1".into(), "key2".into()]), Some(true));
427        assert!(data.is_ok());
428        assert_eq!(
429            data.unwrap(),
430            ndarray::array![[1u32.into(), 2u32.into()], [11u32.into(), 21u32.into()]]
431        );
432    }
433
434    #[cfg(feature = "python")]
435    #[rstest]
436    fn test_from_create() {
437        pyo3::Python::with_gil(|_py| {
438            let mut hm: HashMap<String, Vec<DataValue>> = Default::default();
439            let value: Vec<DataValue> = vec![1i32.into(), 22i32.into()];
440            hm.insert("a".into(), value);
441
442            let df = DataFrame::from_dict_np(hm);
443            assert_eq!(
444                df.select(Some(&["a".into()])),
445                Ok(ndarray::array![
446                    [DataValue::from(1i32)],
447                    [DataValue::from(22i32)]
448                ]),
449            );
450        });
451        #[cfg(feature = "polars-df")]
452        {
453            let pdf = polars::df!(
454                "a" => [1u64, 2u64, 3u64],
455                "b" => [4f64, 5f64, 6f64],
456                "c" => [7i64, 8i64, 9i64]
457            )
458            .expect("BUG: should be ok");
459            let df = DataFrame::from_polars(pyo3_polars::PyDataFrame(pdf));
460            assert_eq!(
461                df.select(Some(&["a".into(), "b".into(), "c".into()])),
462                crate::df! {
463                    "a" => [1u64, 2u64, 3u64],
464                    "b" => [4f64, 5f64, 6f64],
465                    "c" => [7i64, 8i64, 9i64]
466                }
467                .select(Some(&["a".into(), "b".into(), "c".into()])),
468            );
469        }
470    }
471
472    #[rstest]
473    #[traced_test]
474    fn basic_ops_add(mut df: DataFrame, hm: HashMap<String, DataValue>) {
475        let mut df_expect = df.clone();
476        let df2 = df.clone();
477        let exec = Python::with_gil(|py| -> PyResult<()> {
478            df.__iadd__(df.clone().into_pyobject(py)?.into_any())?;
479            df_expect.dataframe += df2.dataframe;
480            tracing::trace!("{} vs {}", df, df_expect);
481            assert_eq!(df.dataframe, df_expect.dataframe);
482
483            df.__iadd__(hm.clone().into_pyobject(py)?.into_any())?;
484            df_expect.dataframe += hm;
485            tracing::trace!("{} vs {}", df, df_expect);
486            assert_eq!(df.dataframe, df_expect.dataframe);
487
488            Ok(())
489        });
490
491        assert!(exec.is_ok(), "{:?}", exec);
492    }
493
494    #[rstest]
495    #[traced_test]
496    fn basic_ops_sub(mut df: DataFrame, hm: HashMap<String, DataValue>) {
497        let mut df_expect = df.clone();
498        let df2 = df.clone();
499        let exec = Python::with_gil(|py| -> PyResult<()> {
500            df.__isub__(df.clone().into_pyobject(py)?.into_any())?;
501            df_expect.dataframe -= df2.dataframe;
502            tracing::trace!("{} vs {}", df, df_expect);
503            assert_eq!(df.dataframe, df_expect.dataframe);
504
505            df.__isub__(hm.clone().into_pyobject(py)?.into_any())?;
506            df_expect.dataframe -= hm;
507            tracing::trace!("{} vs {}", df, df_expect);
508            assert_eq!(df.dataframe, df_expect.dataframe);
509
510            Ok(())
511        });
512
513        assert!(exec.is_ok(), "{:?}", exec);
514    }
515
516    #[rstest]
517    #[traced_test]
518    fn basic_ops_mul(mut df: DataFrame, hm: HashMap<String, DataValue>) {
519        let mut df_expect = df.clone();
520        let df2 = df.clone();
521        let exec = Python::with_gil(|py| -> PyResult<()> {
522            df.__imul__(df.clone().into_pyobject(py)?.into_any())?;
523            df_expect.dataframe *= df2.dataframe;
524            tracing::trace!("{} vs {}", df, df_expect);
525            assert_eq!(df.dataframe, df_expect.dataframe);
526
527            df.__imul__(hm.clone().into_pyobject(py)?.into_any())?;
528            df_expect.dataframe *= hm;
529            tracing::trace!("{} vs {}", df, df_expect);
530            assert_eq!(df.dataframe, df_expect.dataframe);
531            Ok(())
532        });
533
534        assert!(exec.is_ok(), "{:?}", exec);
535    }
536
537    #[rstest]
538    #[traced_test]
539    fn basic_ops_div(mut df: DataFrame, hm: HashMap<String, DataValue>) {
540        let mut df_expect = df.clone();
541        let df2 = df.clone();
542        let exec = Python::with_gil(|py| -> PyResult<()> {
543            df.__itruediv__(df.clone().into_pyobject(py)?.into_any())?;
544            df_expect.dataframe /= df2.dataframe;
545            tracing::trace!("{} vs {}", df, df_expect);
546            assert_eq!(df.dataframe, df_expect.dataframe);
547
548            df.__itruediv__(hm.clone().into_pyobject(py)?.into_any())?;
549            df_expect.dataframe /= hm;
550            tracing::trace!("{} vs {}", df, df_expect);
551            assert_eq!(df.dataframe, df_expect.dataframe);
552            Ok(())
553        });
554
555        assert!(exec.is_ok(), "{:?}", exec);
556    }
557
558    #[rstest]
559    #[traced_test]
560    #[rstest]
561    fn test_numpy(mut df: DataFrame) {
562        let exec = Python::with_gil(|py| -> PyResult<()> {
563            let code = c_str!(
564                r#"
565def example(df):
566    import numpy as np
567    a_np = df.as_numpy_f32(['key1', 'key2'])
568    print(a_np)
569    b_np = df.as_numpy_u32(['key1', 'key'])
570    print(b_np)
571    b_np = df.as_numpy_i32(['key1', 'key'])
572    print(b_np)
573    b_np = df.as_numpy_i64(['key1', 'key'])
574    print(b_np)
575    b_np = df.as_numpy_u64(['key1', 'key'])
576    print(b_np)
577    b_np = df.as_numpy_f64(['key1', 'key'])
578    print(b_np)
579    b_np = df.as_numpy_f64(['key1', 'key'], transposed=True)
580    print(b_np)
581    return df
582            "#
583            );
584            let fun: Py<PyAny> = PyModule::from_code(py, code, c_str!(""), c_str!(""))?
585                .getattr("example")?
586                .into();
587            let result = fun.call1(py, (df.clone(),));
588            assert!(df.py_join(df.clone(), JoinRelation::default()).is_ok());
589            // user may not have installed polars, we need to get an error in that
590            // case
591            if py.import("numpy").is_ok() {
592                assert!(result.is_ok(), "{:?}", result);
593            } else {
594                assert!(result.is_err(), "{:?}", result);
595            }
596            Ok(())
597        });
598        assert!(exec.is_ok(), "{:?}", exec);
599    }
600
601    #[rstest]
602    #[traced_test]
603    #[rstest]
604    fn test_fill_from_python(df: DataFrame) {
605        let exec = Python::with_gil(|_py| -> PyResult<()> {
606            let hm = stdhashmap!(
607                Key::from("key1") => DataValue::U32(1),
608                Key::from("key2") => DataValue::U32(2),
609            );
610            let mut df2 = DataFrame::init();
611            assert!(df2.py_push(hm).is_ok());
612            assert!(df2
613                .py_push(stdhashmap!(
614                    Key::from("key1") => DataValue::U32(11),
615                    Key::from("key2") => DataValue::U32(21),
616                ))
617                .is_ok());
618
619            assert_eq!(df, df2);
620
621            let mut df2 = DataFrame::init();
622            assert!(df2
623                .py_add_column(
624                    Key::from("key1"),
625                    vec![DataValue::U32(1), DataValue::U32(11)]
626                )
627                .is_ok());
628            assert!(df2
629                .py_add_column(
630                    Key::from("key2"),
631                    vec![DataValue::U32(2), DataValue::U32(21)]
632                )
633                .is_ok());
634
635            assert_eq!(df, df2);
636            Ok(())
637        });
638        assert!(exec.is_ok(), "{:?}", exec);
639    }
640
641    #[rstest]
642    fn basic_python_dataframe(mut df: DataFrame) {
643        let exec = Python::with_gil(|py| -> PyResult<()> {
644            let fun: Py<PyAny> = PyModule::from_code(
645                py,
646                c_str!(
647                    "
648def example(df):
649    print(df)
650    df.shrink()
651    assert len(df) == 2
652    df.add_alias('key1', 'key1-alias')
653    a = df.select(['key1', 'key2'])
654    print(a)
655    b = df.select(['key1-alias', 'key2'])
656    print(b)
657    df.rename_key('key1', 'key1new')
658    df.rename_key('key1new', 'key1')
659    assert a == [[1, 2], [11, 21]]
660    assert a == b
661    df.add_metadata('test', 1)
662    m = df.get_metadata('test')
663    assert m == 1
664    b = df.select_transposed(['key1', 'key2'])
665    print(b)
666    assert b == [[1, 11], [2, 21]]
667    c = df.select_column('key1')
668    print(c)
669    assert c == [1, 11]
670
671    a += b
672    print(a)
673    assert a == [[2, 13], [4, 23]]
674    a -= b
675    print(a)
676    assert e == a
677    f = e * b
678    print(f)
679    assert f == [[1, 22], [44, 441]]
680    g = f / b
681    print(g)
682    assert g == e
683
684                "
685                ),
686                c_str!(""),
687                c_str!(""),
688            )?
689            .getattr("example")?
690            .into();
691            let _ = fun.call1(py, (df.clone(),));
692            assert!(df.py_join(df.clone(), JoinRelation::default()).is_ok());
693            Ok(())
694        });
695        assert!(exec.is_ok(), "{:?}", exec);
696    }
697
698    #[rstest]
699    fn dummy_test_apply(mut df: DataFrame) {
700        let exec = Python::with_gil(|py| -> PyResult<()> {
701            let fun: Py<PyAny> = PyModule::from_code(
702                py,
703                c_str!(
704                    r#"
705def multiply_by_ten(x):
706    print(x)
707    x *= {"key1": 10}
708    print(x)
709    return x
710
711def example(df):
712    print(df)
713    df.apply(multiply_by_ten)
714                "#
715                ),
716                c_str!(""),
717                c_str!(""),
718            )?
719            .getattr("example")?
720            .into();
721            let _ = fun.call1(py, (df.clone(),));
722            assert!(df.py_join(df.clone(), JoinRelation::default()).is_ok());
723            Ok(())
724        });
725        assert!(exec.is_ok(), "{:?}", exec);
726    }
727}