Skip to content
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

implement Serialize, Deserialize for Py<T> #1366

Merged
merged 1 commit into from Jan 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Expand Up @@ -91,22 +91,22 @@ jobs:
run: cargo build --no-default-features --verbose --target ${{ matrix.platform.rust-target }}

- name: Build (all additive features)
run: cargo build --no-default-features --features "macros num-bigint num-complex hashbrown" --verbose --target ${{ matrix.platform.rust-target }}
run: cargo build --no-default-features --features "macros num-bigint num-complex hashbrown serde" --verbose --target ${{ matrix.platform.rust-target }}

# Run tests (except on PyPy, because no embedding API).
- if: matrix.python-version != 'pypy-3.6'
name: Test
run: cargo test --no-default-features --features "macros num-bigint num-complex hashbrown" --target ${{ matrix.platform.rust-target }}
run: cargo test --no-default-features --features "macros num-bigint num-complex hashbrown serde" --target ${{ matrix.platform.rust-target }}

# Run tests again, but in abi3 mode
- if: matrix.python-version != 'pypy-3.6'
name: Test (abi3)
run: cargo test --no-default-features --features "abi3 macros num-bigint num-complex hashbrown" --target ${{ matrix.platform.rust-target }}
run: cargo test --no-default-features --features "abi3 macros num-bigint num-complex hashbrown serde" --target ${{ matrix.platform.rust-target }}

# Run tests again, for abi3-py36 (the minimal Python version)
- if: (matrix.python-version != 'pypy-3.6') && (matrix.python-version != '3.6')
name: Test (abi3-py36)
run: cargo test --no-default-features --features "abi3-py36 macros num-bigint num-complex hashbrown" --target ${{ matrix.platform.rust-target }}
run: cargo test --no-default-features --features "abi3-py36 macros num-bigint num-complex hashbrown serde" --target ${{ matrix.platform.rust-target }}

- name: Test proc-macro code
run: cargo test --manifest-path=pyo3-macros-backend/Cargo.toml --target ${{ matrix.platform.rust-target }}
Expand Down Expand Up @@ -143,7 +143,7 @@ jobs:
- uses: actions-rs/cargo@v1
with:
command: test
args: --features "num-bigint num-complex hashbrown" --no-fail-fast
args: --features "num-bigint num-complex hashbrown serde" --no-fail-fast
env:
CARGO_INCREMENTAL: 0
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -5,6 +5,10 @@ PyO3 versions, please see the [migration guide](https://pyo3.rs/master/migration
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Add `serde` feature to support `Serialize/Deserialize` for `Py<T>`. [#1366](https://github.com/PyO3/pyo3/pull/1366)

## [0.13.1] - 2021-01-10
### Added
- Add support for `#[pyclass(dict)]` and `#[pyclass(weakref)]` with the `abi3` feature on Python 3.9 and up. [#1342](https://github.com/PyO3/pyo3/pull/1342)
Expand Down
2 changes: 2 additions & 0 deletions Cargo.toml
Expand Up @@ -27,6 +27,7 @@ paste = { version = "1.0.3", optional = true }
pyo3-macros = { path = "pyo3-macros", version = "=0.13.1", optional = true }
unindent = { version = "0.1.4", optional = true }
hashbrown = { version = "0.9", optional = true }
serde = {version = "1.0", optional = true}

[dev-dependencies]
assert_approx_eq = "1.1.0"
Expand All @@ -35,6 +36,7 @@ rustversion = "1.0"
proptest = { version = "0.10.1", default-features = false, features = ["std"] }
# features needed to run the PyO3 test suite
pyo3 = { path = ".", default-features = false, features = ["macros", "auto-initialize"] }
serde_json = "1.0.61"

[features]
default = ["macros", "auto-initialize"]
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Expand Up @@ -12,8 +12,8 @@ fmt:

clippy:
@touch src/lib.rs # Touching file to ensure that cargo clippy will re-check the project
cargo clippy --features="num-bigint num-complex hashbrown" --tests -- -Dwarnings
cargo clippy --features="abi3 num-bigint num-complex hashbrown" --tests -- -Dwarnings
cargo clippy --features="num-bigint num-complex hashbrown serde" --tests -- -Dwarnings
cargo clippy --features="abi3 num-bigint num-complex hashbrown serde" --tests -- -Dwarnings
for example in examples/*; do cargo clippy --manifest-path $$example/Cargo.toml -- -Dwarnings || exit 1; done

lint: fmt clippy
Expand Down
21 changes: 21 additions & 0 deletions guide/src/features.md
Expand Up @@ -62,3 +62,24 @@ These macros require a number of dependencies which may not be needed by users w
The `nightly` feature needs the nightly Rust compiler. This allows PyO3 to use Rust's unstable specialization feature to apply the following optimizations:
- `FromPyObject` for `Vec` and `[T;N]` can perform a `memcpy` when the object supports the Python buffer protocol.
- `ToBorrowedObject` can skip a reference count increase when the provided object is a Python native type.

### `serde`

The `serde` feature enables (de)serialization of Py<T> objects via [serde](https://serde.rs/).
This allows to use [`#[derive(Serialize, Deserialize)`](https://serde.rs/derive.html) on structs that hold references to `#[pyclass]` instances

```rust

#[pyclass]
#[derive(Serialize, Deserialize)]
struct Permission {
name: String
}

#[pyclass]
#[derive(Serialize, Deserialize)]
struct User {
username: String,
permissions: Vec<Py<Permission>>
}
```
3 changes: 3 additions & 0 deletions src/lib.rs
Expand Up @@ -207,6 +207,9 @@ mod python;
pub mod type_object;
pub mod types;

#[cfg(feature = "serde")]
pub mod serde;

/// The proc macros, which are also part of the prelude.
#[cfg(feature = "macros")]
pub mod proc_macro {
Expand Down
36 changes: 36 additions & 0 deletions src/serde.rs
@@ -0,0 +1,36 @@
use crate::type_object::PyBorrowFlagLayout;
use crate::{Py, PyClass, PyClassInitializer, PyTypeInfo, Python};
use serde::{de, ser, Deserialize, Deserializer, Serialize, Serializer};

impl<T> Serialize for Py<T>
where
T: Serialize + PyClass,
{
fn serialize<S>(&self, serializer: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
where
S: Serializer,
{
Python::with_gil(|py| {
self.try_borrow(py)
.map_err(|e| ser::Error::custom(e.to_string()))?
.serialize(serializer)
})
}
}

impl<'de, T> Deserialize<'de> for Py<T>
where
T: Into<PyClassInitializer<T>> + PyClass + Deserialize<'de>,
<T as PyTypeInfo>::BaseLayout: PyBorrowFlagLayout<<T as PyTypeInfo>::BaseType>,
{
fn deserialize<D>(deserializer: D) -> Result<Py<T>, D::Error>
where
D: Deserializer<'de>,
{
let deserialized = T::deserialize(deserializer)?;

Python::with_gil(|py| {
Py::new(py, deserialized).map_err(|e| de::Error::custom(e.to_string()))
})
}
}
79 changes: 79 additions & 0 deletions tests/test_serde.rs
@@ -0,0 +1,79 @@
#[cfg(feature = "serde")]
mod test_serde {
use pyo3::prelude::*;

use serde::{Deserialize, Serialize};

#[pyclass]
#[derive(Debug, Serialize, Deserialize)]
struct Group {
name: String,
}

#[pyclass]
#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
username: String,
group: Option<Py<Group>>,
friends: Vec<Py<User>>,
}

#[test]
fn test_serialize() {
let friend1 = User {
username: "friend 1".into(),
group: None,
friends: vec![],
};
let friend2 = User {
username: "friend 2".into(),
..friend1.clone()
};

let user = Python::with_gil(|py| {
let py_friend1 = Py::new(py, friend1).expect("failed to create friend 1");
let py_friend2 = Py::new(py, friend2).expect("failed to create friend 2");

let friends = vec![py_friend1, py_friend2];
let py_group = Py::new(
py,
Group {
name: "group name".into(),
},
)
.unwrap();

User {
username: "danya".into(),
group: Some(py_group),
friends,
}
});

let serialized = serde_json::to_string(&user).expect("failed to serialize");
assert_eq!(
serialized,
r#"{"username":"danya","group":{"name":"group name"},"friends":[{"username":"friend 1","group":null,"friends":[]},{"username":"friend 2","group":null,"friends":[]}]}"#
);
}

#[test]
fn test_deserialize() {
let serialized = r#"{"username": "danya", "friends":
[{"username": "friend", "group": {"name": "danya's friends"}, "friends": []}]}"#;
let user: User = serde_json::from_str(serialized).expect("failed to deserialize");

assert_eq!(user.username, "danya");
assert_eq!(user.group, None);
assert_eq!(user.friends.len(), 1usize);
let friend = user.friends.get(0).unwrap();

Python::with_gil(|py| {
assert_eq!(friend.borrow(py).username, "friend");
assert_eq!(
friend.borrow(py).group.as_ref().unwrap().borrow(py).name,
"danya's friends"
)
});
}
}