Repository: attack68/rateslib (language: python)

Issue:
Serializing curves and solvers relying on multi-currency instruments
Hi !
Upon trying to serialize my differents curves it triggered :

``` python
TypeError: cannot pickle 'rateslib.rs.FXRates' object
```

I understand from [doc](https://rateslib.readthedocs.io/en/1.4.x/u_serialization.html#fxrates) that serializing from FXRates is, for now, done by hand by calling to_json.

Do you recommend tweaking all the classes relying on FXRates to make them serializable or is there a workaround/improvement scheduled for this purpose ?

Thanks !


Discussion / hints:
You are trying to serialize with Python's native `pickle` library?

Fix patch:
diff --git a/docs/source/i_whatsnew.rst b/docs/source/i_whatsnew.rst
index 4cee8590..7b9087f2 100644
--- a/docs/source/i_whatsnew.rst
+++ b/docs/source/i_whatsnew.rst
@@ -50,6 +50,10 @@ email contact through **rateslib@gmail.com**.
      - Improve the speed of bond :meth:`~rateslib.instruments.FixedRateBond.ytm` calculations from about 750us to
        500us on average.
        `(380) <https://github.com/attack68/rateslib/pull/380>`_
+   * - Bug
+     - :class:`~rateslib.fx.FXRates` fix support for pickling which allows multithreading across CPU pools or
+       external serialization.
+       `(393) <https://github.com/attack68/rateslib/pull/393>`_
    * - Bug
      - The ``eom`` parameter for spec *"us_gb"* and *"us_gb_tsy"* and associated aliases is corrected to *True*.
        `(368) <https://github.com/attack68/rateslib/pull/368>`_
diff --git a/rust/fx/rates_py.rs b/rust/fx/rates_py.rs
index c28988c5..6698172c 100644
--- a/rust/fx/rates_py.rs
+++ b/rust/fx/rates_py.rs
@@ -2,15 +2,16 @@
 
 use crate::dual::{ADOrder, Number, NumberArray2};
 use crate::fx::rates::{Ccy, FXRate, FXRates};
+use bincode::{deserialize, serialize};
 use chrono::prelude::*;
 use ndarray::Axis;
 use pyo3::prelude::*;
 // use std::collections::HashMap;
 use pyo3::exceptions::PyValueError;
 // use pyo3::exceptions::PyValueError;
-// use pyo3::types::PyFloat;
 use crate::json::json_py::DeserializedObj;
 use crate::json::JSON;
+use pyo3::types::PyBytes;
 
 #[pymethods]
 impl Ccy {
@@ -32,6 +33,18 @@ impl Ccy {
     fn __eq__(&self, other: &Self) -> bool {
         self.name == other.name
     }
+
+    // Pickling
+    pub fn __setstate__(&mut self, state: Bound<'_, PyBytes>) -> PyResult<()> {
+        *self = deserialize(state.as_bytes()).unwrap();
+        Ok(())
+    }
+    pub fn __getstate__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
+        Ok(PyBytes::new_bound(py, &serialize(&self).unwrap()))
+    }
+    pub fn __getnewargs__<'py>(&self) -> PyResult<(String,)> {
+        Ok(((*(self.name)).clone(),))
+    }
 }
 
 #[pymethods]
@@ -93,6 +106,23 @@ impl FXRate {
     fn __eq__(&self, other: &Self) -> bool {
         self == other
     }
+
+    // Pickling
+    pub fn __setstate__(&mut self, state: Bound<'_, PyBytes>) -> PyResult<()> {
+        *self = deserialize(state.as_bytes()).unwrap();
+        Ok(())
+    }
+    pub fn __getstate__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
+        Ok(PyBytes::new_bound(py, &serialize(&self).unwrap()))
+    }
+    pub fn __getnewargs__<'py>(&self) -> PyResult<(String, String, Number, Option<NaiveDateTime>)> {
+        Ok((
+            (*(self.pair.0.name)).clone(),
+            (*(self.pair.1.name)).clone(),
+            self.rate.clone(),
+            self.settlement,
+        ))
+    }
 }
 
 #[pymethods]
@@ -216,10 +246,20 @@ impl FXRates {
         }
     }
 
+    // Pickling
+    pub fn __setstate__(&mut self, state: Bound<'_, PyBytes>) -> PyResult<()> {
+        *self = deserialize(state.as_bytes()).unwrap();
+        Ok(())
+    }
+    pub fn __getstate__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
+        Ok(PyBytes::new_bound(py, &serialize(&self).unwrap()))
+    }
+    pub fn __getnewargs__<'py>(&self) -> PyResult<(Vec<FXRate>, Option<Ccy>)> {
+        Ok((self.fx_rates.clone(), Some(self.currencies[0])))
+    }
+
     // Equality
     fn __eq__(&self, other: FXRates) -> bool {
-        println!("{:?}", self);
-        println!("{:?}", other);
         self.eq(&other)
     }
 


Test patch:
diff --git a/python/tests/test_fx.py b/python/tests/test_fx.py
index 8d084cc0..09aa495d 100644
--- a/python/tests/test_fx.py
+++ b/python/tests/test_fx.py
@@ -46,6 +46,14 @@ def test_rates() -> None:
     assert fxr.rate("eurgbp") == Dual(1.25, ["fx_usdeur", "fx_usdgbp"], [-0.625, 0.50])
 
 
+def test_fxrates_pickle():
+    fxr = FXRates({"usdeur": 2.0, "usdgbp": 2.5}, settlement=dt(2002, 1, 1))
+    import pickle
+    pickled = pickle.dumps(fxr)
+    result = pickle.loads(pickled)
+    assert result == fxr
+
+
 def test_rates_repr():
     fxr = FXRates({"usdeur": 2.0, "usdgbp": 2.5})
     result = fxr.__repr__()