Skip to content

feat: expose a visitor to deserialize a pyobject #65

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Unreleased

- Add `pyobject` module for serialize and deserialize `PyObject`
- Add `PyObjectVisitor` that can be used to implement deserialize
- Add feature `serde_with`

## 0.21.1 - 2024-04-02

- Fix compile error when using PyO3 `abi3` feature targeting a minimum version below 3.10
Expand Down
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ homepage = "https://github.com/davidhewitt/pythonize"
repository = "https://github.com/davidhewitt/pythonize"
documentation = "https://docs.rs/crate/pythonize/"

[features]
default = ["serde_with"]
serde_with = ["serde-transcode"]

[dependencies]
serde = { version = "1.0", default-features = false, features = ["std"] }
pyo3 = { version = "0.21.0", default-features = false }
serde-transcode = { version = "1.0", default-features = false, optional = true }


[dev-dependencies]
serde = { version = "1.0", default-features = false, features = ["derive"] }
Expand Down
39 changes: 39 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
mod de;
mod error;
mod ser;
mod visitor;

#[allow(deprecated)]
pub use crate::de::depythonize;
Expand All @@ -48,3 +49,41 @@ pub use crate::ser::{
pythonize, pythonize_custom, PythonizeDefault, PythonizeDictType, PythonizeListType,
PythonizeTypes, Pythonizer,
};
pub use crate::visitor::PyObjectVisitor;

#[cfg(feature = "serde_with")]
/// This module provides a Serde `Serialize` and `Deserialize` implementation for `PyObject`.
///
/// ```rust
/// use pyo3::PyObject;
/// use serde::{Serialize, Deserialize};
///
/// #[derive(Serialize, Deserialize)]
/// struct Foo {
/// #[serde(with = "pythonize::pyobject")]
/// #[serde(flatten)]
/// inner: PyObject,
/// }
/// ```
pub mod pyobject {
use pyo3::{PyObject, Python};
use serde::Serializer;

pub fn serialize<S>(obj: &PyObject, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Python::with_gil(|py| {
let mut deserializer =
crate::Depythonizer::from_object_bound(obj.clone().into_bound(py));
serde_transcode::transcode(&mut deserializer, serializer)
})
}

pub fn deserialize<'de, D>(deserializer: D) -> Result<PyObject, D::Error>
where
D: serde::Deserializer<'de>,
{
Python::with_gil(|py| deserializer.deserialize_any(crate::PyObjectVisitor::new(py)))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also use serde_transcode? I think in that way the PyObjectVisitor might not be needed, and instead we use Pythonizer here in the same way we use Depythonizer above?

}
}
183 changes: 183 additions & 0 deletions src/visitor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
use pyo3::{
types::{PyDict, PyDictMethods, PyList, PyListMethods},
PyObject, Python, ToPyObject,
};
use serde::{
de::{MapAccess, SeqAccess, VariantAccess, Visitor},
Deserialize,
};

pub struct PyObjectVisitor<'py> {
py: Python<'py>,
}

impl<'py> PyObjectVisitor<'py> {
pub fn new(py: Python) -> PyObjectVisitor<'_> {
PyObjectVisitor { py }
}
}

struct Wrapper {
inner: PyObject,
}

impl<'de> Deserialize<'de> for Wrapper {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Python::with_gil(|py| {
deserializer
.deserialize_any(PyObjectVisitor { py })
.map(|inner| Wrapper { inner })
})
}
}

impl<'de, 'py> Visitor<'de> for PyObjectVisitor<'py> {
type Value = PyObject;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("any PyObject")
}

fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
Ok(value.to_object(self.py))
}

fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
Ok(value.to_object(self.py))
}

fn visit_i128<E>(self, value: i128) -> Result<Self::Value, E> {
Ok(value.to_object(self.py))
}

fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
Ok(value.to_object(self.py))
}

fn visit_u128<E>(self, value: u128) -> Result<Self::Value, E> {
Ok(value.to_object(self.py))
}

fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E> {
Ok(value.to_object(self.py))
}

fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
Ok(value.to_object(self.py))
}

fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
Ok(value.to_object(self.py))
}

fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> {
Ok(v.to_object(self.py))
}

fn visit_none<E>(self) -> Result<Self::Value, E> {
Ok(self.py.None())
}

fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
let wrapper: Wrapper = Deserialize::deserialize(deserializer)?;
Ok(wrapper.inner)
}

fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
self.visit_none()
}

fn visit_seq<V>(self, mut visitor: V) -> Result<Self::Value, V::Error>
where
V: SeqAccess<'de>,
{
let list = PyList::empty_bound(self.py);

while let Some(wrapper) = visitor.next_element::<Wrapper>()? {
list.append(wrapper.inner).unwrap();
}

Ok(list.to_object(self.py))
}

fn visit_map<V>(self, mut visitor: V) -> Result<Self::Value, V::Error>
where
V: MapAccess<'de>,
{
let dict = PyDict::new_bound(self.py);

while let Some((key, value)) = visitor.next_entry::<Wrapper, Wrapper>()? {
dict.set_item(key.inner, value.inner).unwrap();
}

Ok(dict.to_object(self.py))
}

fn visit_enum<A>(self, data: A) -> Result<Self::Value, A::Error>
where
A: serde::de::EnumAccess<'de>,
{
let dict = PyDict::new_bound(self.py);
let (value, variant): (Wrapper, _) = data.variant()?;
let variant: Wrapper = variant.newtype_variant()?;
dict.set_item(variant.inner, value.inner).unwrap();

Ok(dict.to_object(self.py))
}
}

#[cfg(test)]
mod tests {
use pyo3::{
types::{PyAnyMethods, PyStringMethods},
PyObject, Python,
};
use serde::{Deserialize, Serialize};
use serde_json::json;

#[derive(Serialize, Deserialize)]
struct Foo {
#[serde(with = "crate::pyobject")]
#[serde(flatten)]
inner: PyObject,
}

#[test]
fn simple_test() {
let value = json!({
"code": 200,
"success": true,
"payload": {
"features": [
"serde",
"json"
],
"homepage": null
}
});

Python::with_gil(|py| {
let foo = Foo {
inner: crate::pythonize(py, &value).unwrap(),
};
let serialized = serde_json::to_string(&foo).unwrap();
let deserialized: Foo = serde_json::from_str(&serialized).unwrap();
assert_eq!(
deserialized
.inner
.bind(py)
.repr()
.unwrap()
.to_str()
.unwrap(),
foo.inner.bind(py).repr().unwrap().to_str().unwrap()
);
});
}
}