├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── release.yml │ └── ci.yml ├── .gitignore ├── src ├── lib.rs ├── error.rs ├── ser.rs └── de.rs ├── Cargo.toml ├── LICENSE ├── README.md ├── CHANGELOG.md └── tests ├── test_with_serde_path_to_err.rs └── test_custom_types.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: davidhewitt 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .idea 4 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | mod de; 4 | mod error; 5 | mod ser; 6 | 7 | pub use crate::de::{depythonize, Depythonizer}; 8 | pub use crate::error::{PythonizeError, Result}; 9 | pub use crate::ser::{ 10 | pythonize, pythonize_custom, PythonizeDefault, PythonizeListType, PythonizeMappingType, 11 | PythonizeNamedMappingType, PythonizeTypes, PythonizeUnnamedMappingAdapter, Pythonizer, 12 | }; 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Rust Crate 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | workflow_dispatch: 8 | inputs: 9 | version: 10 | description: The version to build 11 | 12 | jobs: 13 | release: 14 | permissions: 15 | id-token: write 16 | 17 | runs-on: ubuntu-latest 18 | environment: release 19 | steps: 20 | - uses: actions/checkout@v5 21 | with: 22 | # The tag to build or the tag received by the tag event 23 | ref: ${{ github.event.inputs.version || github.ref }} 24 | persist-credentials: false 25 | 26 | - uses: rust-lang/crates-io-auth-action@v1 27 | id: auth 28 | 29 | - name: Publish to crates.io 30 | run: cargo publish 31 | env: 32 | CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} 33 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pythonize" 3 | version = "0.27.0" 4 | authors = ["David Hewitt <1939362+davidhewitt@users.noreply.github.com>"] 5 | edition = "2021" 6 | rust-version = "1.74" 7 | license = "MIT" 8 | description = "Serde Serializer & Deserializer from Rust <--> Python, backed by PyO3." 9 | homepage = "https://github.com/davidhewitt/pythonize" 10 | repository = "https://github.com/davidhewitt/pythonize" 11 | documentation = "https://docs.rs/crate/pythonize/" 12 | 13 | 14 | [dependencies] 15 | serde = { version = "1.0", default-features = false, features = ["std"] } 16 | pyo3 = { version = "0.27", default-features = false } 17 | 18 | [dev-dependencies] 19 | serde = { version = "1.0", default-features = false, features = ["derive"] } 20 | pyo3 = { version = "0.27", default-features = false, features = ["auto-initialize", "macros", "py-clone"] } 21 | serde_json = "1.0" 22 | serde_bytes = "0.11" 23 | maplit = "1.0.2" 24 | serde_path_to_error = "0.1.15" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022-present David Hewitt and Contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pythonize 2 | 3 | This is an experimental serializer for Rust's serde ecosystem, which can convert Rust objects to Python values and back. 4 | 5 | At the moment the Python structures it produces should be _very_ similar to those which are produced by `serde_json`; i.e. calling Python's `json.loads()` on a value encoded by `serde_json` should produce an identical structure to 6 | that which is produced directly by `pythonize`. 7 | 8 | ## Usage 9 | 10 | This crate converts Rust types which implement the [Serde] serialization 11 | traits into Python objects using the [PyO3] library. 12 | 13 | Pythonize has two main public APIs: `pythonize` and `depythonize`. 14 | 15 | 16 | 17 | [Serde]: https://github.com/serde-rs/serde 18 | [PyO3]: https://github.com/PyO3/pyo3 19 | 20 | # Examples 21 | 22 | ```rust 23 | use serde::{Serialize, Deserialize}; 24 | use pyo3::prelude::*; 25 | use pythonize::{depythonize, pythonize}; 26 | 27 | #[derive(Debug, Serialize, Deserialize, PartialEq)] 28 | struct Sample { 29 | foo: String, 30 | bar: Option 31 | } 32 | 33 | let sample = Sample { 34 | foo: "Foo".to_string(), 35 | bar: None 36 | }; 37 | 38 | Python::attach(|py| { 39 | // Rust -> Python 40 | let obj = pythonize(py, &sample).unwrap(); 41 | 42 | assert_eq!("{'foo': 'Foo', 'bar': None}", &format!("{}", obj.repr().unwrap())); 43 | 44 | // Python -> Rust 45 | let new_sample: Sample = depythonize(&obj).unwrap(); 46 | 47 | assert_eq!(new_sample, sample); 48 | }) 49 | ``` 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.27.0 - 2025-11-07 2 | - Update to PyO3 0.27 3 | 4 | ## 0.26.0 - 2025-08-30 5 | 6 | ### Packaging 7 | - Bump MSRV to 1.74 8 | - Update to PyO3 0.26 9 | 10 | ### Changed 11 | - `PythonizeTypes`, `PythonizeMappingType` and `PythonizeNamedMappingType` no longer have a lifetime on the trait, instead the `Builder` type is a GAT. 12 | 13 | ## 0.25.0 - 2025-05-23 14 | 15 | ### Packaging 16 | - Update to PyO3 0.25 17 | 18 | ## 0.24.0 - 2025-03-26 19 | 20 | ### Packaging 21 | - Update to PyO3 0.24 22 | 23 | ## Removed 24 | - Remove deprecated `depythonize_bound()` 25 | 26 | ## 0.23.0 - 2024-11-22 27 | 28 | ### Packaging 29 | - Update to PyO3 0.23 30 | 31 | ## 0.22.0 - 2024-08-10 32 | 33 | ### Packaging 34 | - Bump MSRV to 1.63 35 | - Update to PyO3 0.22 36 | 37 | ### Added 38 | - Support `u128` / `i128` integers. 39 | - Implement `PythonizeListType` for `PyTuple` 40 | - Support deserializing enums from any `PyMapping` instead of just `PyDict` 41 | - Support serializing struct-like types to named mappings using `PythonizeTypes::NamedMap` 42 | 43 | ### Changed 44 | - `pythonize()` now returns `Bound<'py, PyAny>` instead of `Py` 45 | - `depythonize()` now take `&'a Bound` and is no longer deprecated 46 | - `depythonize_bound()` is now deprecated 47 | - `Depythonizer::from_object()` now takes `&'a Bound` and is no longer deprecated 48 | - `Depythonizer` now contains `&'a Bound` and so has an extra lifetime `'a` 49 | 50 | ### Removed 51 | - Remove support for PyO3's `gil-refs` feature 52 | 53 | ### Fixed 54 | - Fix overflow error attempting to depythonize `u64` values greater than `i64::MAX` to types like `serde_json::Value` 55 | - Fix deserializing `set` and `frozenset` into Rust homogeneous containers 56 | 57 | ## 0.21.1 - 2024-04-02 58 | 59 | - Fix compile error when using PyO3 `abi3` feature targeting a minimum version below 3.10 60 | 61 | ## 0.21.0 - 2024-04-01 62 | 63 | - Bump edition to 2021 64 | - Bump MSRV to 1.56 65 | - Update to PyO3 0.21 66 | - Export `PythonizeDefault` 67 | 68 | ## 0.20.0 - 2023-10-15 69 | 70 | - Update to PyO3 0.20 71 | 72 | ## 0.19.0 - 2023-06-11 73 | 74 | - Update to PyO3 0.19 75 | 76 | ## 0.18.0 - 2023-01-22 77 | 78 | - Add LICENSE file to the crate 79 | - Update to PyO3 0.18 80 | 81 | ## 0.17.0 - 2022-08-24 82 | 83 | - Update to PyO3 0.17 84 | 85 | ## 0.16.0 - 2022-03-06 86 | 87 | - Update to PyO3 0.16 88 | 89 | ## 0.15.0 - 2021-11-12 90 | 91 | - Update to PyO3 0.15 92 | - Add `pythonize_custom` for customizing the Python types to serialize to. 93 | - Add support for `depythonize` to handle arbitrary Python sequence and mapping types. 94 | 95 | ## 0.14.0 - 2021-07-05 96 | 97 | - Update to PyO3 0.14 98 | 99 | ## 0.13.0 - 2020-12-28 100 | 101 | - Update to PyO3 0.13 102 | 103 | ## 0.12.1 - 2020-12-08 104 | 105 | - Require `std` feature of `serde`. 106 | - Reduce memory consumption when deserializing sequences. 107 | - Fix deserializing untagged struct enum variants. 108 | - Fix deserializing sequences from Python tuples. 109 | 110 | ## 0.12.0 - 2020-11-22 111 | 112 | - Change release versioning to match `pyo3` major/minor version. 113 | - Implement `depythonizer` 114 | 115 | ## 0.1.0 - 2020-08-12 116 | 117 | - Initial release 118 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | resolve: 14 | runs-on: ubuntu-latest 15 | outputs: 16 | MSRV: ${{ steps.resolve-msrv.outputs.MSRV }} 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.12" 22 | - name: resolve MSRV 23 | id: resolve-msrv 24 | run: echo MSRV=`python -c 'import tomllib; print(tomllib.load(open("Cargo.toml", "rb"))["package"]["rust-version"])'` >> $GITHUB_OUTPUT 25 | 26 | fmt: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: dtolnay/rust-toolchain@stable 31 | with: 32 | components: rustfmt 33 | - name: Check rust formatting (rustfmt) 34 | run: cargo fmt --all -- --check 35 | 36 | clippy: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v4 40 | - uses: dtolnay/rust-toolchain@stable 41 | with: 42 | components: clippy 43 | - run: cargo clippy --all 44 | 45 | build: 46 | needs: [resolve, fmt] # don't wait for clippy as fails rarely and takes longer 47 | name: python${{ matrix.python-version }} ${{ matrix.os }} rust-${{ matrix.rust}} 48 | runs-on: ${{ matrix.os }} 49 | strategy: 50 | fail-fast: false # If one platform fails, allow the rest to keep testing. 51 | matrix: 52 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] 53 | os: ["macos-latest", "ubuntu-latest", "windows-latest"] 54 | rust: [stable] 55 | include: 56 | - python-version: "3.14" 57 | os: "ubuntu-latest" 58 | rust: ${{ needs.resolve.outputs.MSRV }} 59 | - python-version: "3.14" 60 | os: "macos-15-intel" 61 | rust: "stable" 62 | - python-version: "3.14" 63 | os: "ubuntu-24.04-arm" 64 | rust: "stable" 65 | - python-version: "3.14" 66 | os: "windows-11-arm" 67 | rust: "stable" 68 | 69 | steps: 70 | - uses: actions/checkout@v4 71 | 72 | - name: Set up Python ${{ matrix.python-version }} 73 | uses: actions/setup-python@v5 74 | with: 75 | python-version: ${{ matrix.python-version }} 76 | 77 | - name: Install Rust toolchain 78 | uses: dtolnay/rust-toolchain@master 79 | with: 80 | toolchain: ${{ matrix.rust }} 81 | 82 | - uses: Swatinem/rust-cache@v2 83 | continue-on-error: true 84 | 85 | - if: ${{ matrix.rust == needs.resolve.outputs.MSRV }} 86 | name: Set dependencies on MSRV 87 | run: cargo +stable update 88 | env: 89 | CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS: fallback 90 | 91 | - name: Test 92 | run: cargo test --verbose 93 | 94 | - name: Test (abi3) 95 | run: cargo test --verbose --features pyo3/abi3-py37 96 | 97 | env: 98 | RUST_BACKTRACE: 1 99 | 100 | coverage: 101 | needs: [fmt] 102 | runs-on: ubuntu-latest 103 | steps: 104 | - uses: actions/checkout@v4 105 | - uses: Swatinem/rust-cache@v2 106 | continue-on-error: true 107 | - name: Install cargo-llvm-cov 108 | uses: taiki-e/install-action@cargo-llvm-cov 109 | - uses: dtolnay/rust-toolchain@stable 110 | with: 111 | components: llvm-tools-preview 112 | - run: | 113 | cargo llvm-cov clean 114 | cargo llvm-cov --codecov --output-path codecov.json 115 | - uses: codecov/codecov-action@v4 116 | with: 117 | file: codecov.json 118 | token: ${{ secrets.CODECOV_TOKEN }} 119 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use pyo3::PyErr; 2 | use pyo3::{exceptions::*, CastError, CastIntoError}; 3 | use serde::{de, ser}; 4 | use std::convert::Infallible; 5 | use std::error; 6 | use std::fmt::{self, Debug, Display}; 7 | use std::result; 8 | 9 | /// Alias for `std::result::Result` with error type `PythonizeError` 10 | pub type Result = result::Result; 11 | 12 | /// Errors that can occur when serializing/deserializing Python objects 13 | pub struct PythonizeError { 14 | pub(crate) inner: Box, 15 | } 16 | 17 | impl PythonizeError { 18 | pub(crate) fn msg(text: T) -> Self 19 | where 20 | T: ToString, 21 | { 22 | Self { 23 | inner: Box::new(ErrorImpl::Message(text.to_string())), 24 | } 25 | } 26 | 27 | pub(crate) fn unsupported_type(t: T) -> Self 28 | where 29 | T: ToString, 30 | { 31 | Self { 32 | inner: Box::new(ErrorImpl::UnsupportedType(t.to_string())), 33 | } 34 | } 35 | 36 | pub(crate) fn dict_key_not_string() -> Self { 37 | Self { 38 | inner: Box::new(ErrorImpl::DictKeyNotString), 39 | } 40 | } 41 | 42 | pub(crate) fn incorrect_sequence_length(expected: usize, got: usize) -> Self { 43 | Self { 44 | inner: Box::new(ErrorImpl::IncorrectSequenceLength { expected, got }), 45 | } 46 | } 47 | 48 | pub(crate) fn invalid_enum_type() -> Self { 49 | Self { 50 | inner: Box::new(ErrorImpl::InvalidEnumType), 51 | } 52 | } 53 | 54 | pub(crate) fn invalid_length_enum() -> Self { 55 | Self { 56 | inner: Box::new(ErrorImpl::InvalidLengthEnum), 57 | } 58 | } 59 | 60 | pub(crate) fn invalid_length_char() -> Self { 61 | Self { 62 | inner: Box::new(ErrorImpl::InvalidLengthChar), 63 | } 64 | } 65 | } 66 | 67 | /// Error codes for problems that can occur when serializing/deserializing Python objects 68 | #[derive(Debug)] 69 | pub enum ErrorImpl { 70 | /// An error originating from the Python runtime 71 | PyErr(PyErr), 72 | /// Generic error message 73 | Message(String), 74 | /// A Python type not supported by the deserializer 75 | UnsupportedType(String), 76 | /// A `PyAny` object that failed to cast to an expected Python type 77 | UnexpectedType(String), 78 | /// Dict keys should be strings to deserialize to struct fields 79 | DictKeyNotString, 80 | /// Sequence length did not match expected tuple or tuple struct length. 81 | IncorrectSequenceLength { expected: usize, got: usize }, 82 | /// Enum variants should either be dict (tagged) or str (variant) 83 | InvalidEnumType, 84 | /// Tagged enum variants should be a dict with exactly 1 key 85 | InvalidLengthEnum, 86 | /// Expected a `char`, but got a Python str that was not length 1 87 | InvalidLengthChar, 88 | } 89 | 90 | impl error::Error for PythonizeError {} 91 | 92 | impl Display for PythonizeError { 93 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 94 | match self.inner.as_ref() { 95 | ErrorImpl::PyErr(e) => Display::fmt(e, f), 96 | ErrorImpl::Message(s) => Display::fmt(s, f), 97 | ErrorImpl::UnsupportedType(s) => write!(f, "unsupported type {}", s), 98 | ErrorImpl::UnexpectedType(s) => write!(f, "unexpected type: {}", s), 99 | ErrorImpl::DictKeyNotString => f.write_str("dict keys must have type str"), 100 | ErrorImpl::IncorrectSequenceLength { expected, got } => { 101 | write!(f, "expected sequence of length {}, got {}", expected, got) 102 | } 103 | ErrorImpl::InvalidEnumType => f.write_str("expected either a str or dict for enum"), 104 | ErrorImpl::InvalidLengthEnum => { 105 | f.write_str("expected tagged enum dict to have exactly 1 key") 106 | } 107 | ErrorImpl::InvalidLengthChar => f.write_str("expected a str of length 1 for char"), 108 | } 109 | } 110 | } 111 | 112 | impl Debug for PythonizeError { 113 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 114 | self.inner.as_ref().fmt(f) 115 | } 116 | } 117 | 118 | impl ser::Error for PythonizeError { 119 | fn custom(msg: T) -> Self 120 | where 121 | T: Display, 122 | { 123 | Self { 124 | inner: Box::new(ErrorImpl::Message(msg.to_string())), 125 | } 126 | } 127 | } 128 | 129 | impl de::Error for PythonizeError { 130 | fn custom(msg: T) -> Self 131 | where 132 | T: Display, 133 | { 134 | Self { 135 | inner: Box::new(ErrorImpl::Message(msg.to_string())), 136 | } 137 | } 138 | } 139 | 140 | /// Convert an exception raised in Python to a `PythonizeError` 141 | impl From for PythonizeError { 142 | fn from(other: Infallible) -> Self { 143 | match other {} 144 | } 145 | } 146 | 147 | /// Convert an exception raised in Python to a `PythonizeError` 148 | impl From for PythonizeError { 149 | fn from(other: PyErr) -> Self { 150 | Self { 151 | inner: Box::new(ErrorImpl::PyErr(other)), 152 | } 153 | } 154 | } 155 | 156 | /// Handle errors that occur when attempting to use `PyAny::cast` 157 | impl<'a, 'py> From> for PythonizeError { 158 | fn from(other: CastError<'a, 'py>) -> Self { 159 | Self { 160 | inner: Box::new(ErrorImpl::UnexpectedType(other.to_string())), 161 | } 162 | } 163 | } 164 | 165 | /// Handle errors that occur when attempting to use `PyAny::cast` 166 | impl<'py> From> for PythonizeError { 167 | fn from(other: CastIntoError<'py>) -> Self { 168 | Self { 169 | inner: Box::new(ErrorImpl::UnexpectedType(other.to_string())), 170 | } 171 | } 172 | } 173 | 174 | /// Convert a `PythonizeError` to a Python exception 175 | impl From for PyErr { 176 | fn from(other: PythonizeError) -> Self { 177 | match *other.inner { 178 | ErrorImpl::PyErr(e) => e, 179 | ErrorImpl::Message(e) => PyException::new_err(e), 180 | ErrorImpl::UnsupportedType(_) 181 | | ErrorImpl::UnexpectedType(_) 182 | | ErrorImpl::DictKeyNotString 183 | | ErrorImpl::InvalidEnumType => PyTypeError::new_err(other.to_string()), 184 | ErrorImpl::IncorrectSequenceLength { .. } 185 | | ErrorImpl::InvalidLengthEnum 186 | | ErrorImpl::InvalidLengthChar => PyValueError::new_err(other.to_string()), 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /tests/test_with_serde_path_to_err.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use pyo3::{ 4 | prelude::*, 5 | types::{PyDict, PyList}, 6 | }; 7 | use pythonize::{PythonizeTypes, PythonizeUnnamedMappingAdapter}; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] 11 | struct Root { 12 | root_key: String, 13 | root_map: BTreeMap>, 14 | } 15 | 16 | impl<'py, T> PythonizeTypes for Root { 17 | type Map = PyDict; 18 | type NamedMap = PythonizeUnnamedMappingAdapter; 19 | type List = PyList; 20 | } 21 | 22 | #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] 23 | struct Nested { 24 | nested_key: T, 25 | } 26 | 27 | #[derive(Deserialize, Debug, PartialEq, Eq)] 28 | struct CannotSerialize {} 29 | 30 | impl Serialize for CannotSerialize { 31 | fn serialize(&self, _serializer: S) -> Result 32 | where 33 | S: serde::Serializer, 34 | { 35 | Err(serde::ser::Error::custom( 36 | "something went intentionally wrong", 37 | )) 38 | } 39 | } 40 | 41 | #[test] 42 | fn test_de_valid() { 43 | Python::attach(|py| { 44 | let pyroot = PyDict::new(py); 45 | pyroot.set_item("root_key", "root_value").unwrap(); 46 | 47 | let nested = PyDict::new(py); 48 | let nested_0 = PyDict::new(py); 49 | nested_0.set_item("nested_key", "nested_value_0").unwrap(); 50 | nested.set_item("nested_0", nested_0).unwrap(); 51 | let nested_1 = PyDict::new(py); 52 | nested_1.set_item("nested_key", "nested_value_1").unwrap(); 53 | nested.set_item("nested_1", nested_1).unwrap(); 54 | 55 | pyroot.set_item("root_map", nested).unwrap(); 56 | 57 | let de = &mut pythonize::Depythonizer::from_object(&pyroot); 58 | let root: Root = serde_path_to_error::deserialize(de).unwrap(); 59 | 60 | assert_eq!( 61 | root, 62 | Root { 63 | root_key: String::from("root_value"), 64 | root_map: BTreeMap::from([ 65 | ( 66 | String::from("nested_0"), 67 | Nested { 68 | nested_key: String::from("nested_value_0") 69 | } 70 | ), 71 | ( 72 | String::from("nested_1"), 73 | Nested { 74 | nested_key: String::from("nested_value_1") 75 | } 76 | ) 77 | ]) 78 | } 79 | ); 80 | }) 81 | } 82 | 83 | #[test] 84 | fn test_de_invalid() { 85 | Python::attach(|py| { 86 | let pyroot = PyDict::new(py); 87 | pyroot.set_item("root_key", "root_value").unwrap(); 88 | 89 | let nested = PyDict::new(py); 90 | let nested_0 = PyDict::new(py); 91 | nested_0.set_item("nested_key", "nested_value_0").unwrap(); 92 | nested.set_item("nested_0", nested_0).unwrap(); 93 | let nested_1 = PyDict::new(py); 94 | nested_1.set_item("nested_key", 1).unwrap(); 95 | nested.set_item("nested_1", nested_1).unwrap(); 96 | 97 | pyroot.set_item("root_map", nested).unwrap(); 98 | 99 | let de = &mut pythonize::Depythonizer::from_object(&pyroot); 100 | let err = serde_path_to_error::deserialize::<_, Root>(de).unwrap_err(); 101 | 102 | assert_eq!(err.path().to_string(), "root_map.nested_1.nested_key"); 103 | assert_eq!( 104 | err.to_string(), 105 | "root_map.nested_1.nested_key: unexpected type: 'int' object cannot be cast as 'str'" 106 | ); 107 | }) 108 | } 109 | 110 | #[test] 111 | fn test_ser_valid() { 112 | Python::attach(|py| { 113 | let root = Root { 114 | root_key: String::from("root_value"), 115 | root_map: BTreeMap::from([ 116 | ( 117 | String::from("nested_0"), 118 | Nested { 119 | nested_key: String::from("nested_value_0"), 120 | }, 121 | ), 122 | ( 123 | String::from("nested_1"), 124 | Nested { 125 | nested_key: String::from("nested_value_1"), 126 | }, 127 | ), 128 | ]), 129 | }; 130 | 131 | let ser = pythonize::Pythonizer::>::from(py); 132 | let pyroot: Bound<'_, PyAny> = serde_path_to_error::serialize(&root, ser).unwrap(); 133 | 134 | let pyroot = pyroot.cast::().unwrap(); 135 | assert_eq!(pyroot.len(), 2); 136 | 137 | let root_value: String = pyroot 138 | .get_item("root_key") 139 | .unwrap() 140 | .unwrap() 141 | .extract() 142 | .unwrap(); 143 | assert_eq!(root_value, "root_value"); 144 | 145 | let root_map = pyroot 146 | .get_item("root_map") 147 | .unwrap() 148 | .unwrap() 149 | .cast_into::() 150 | .unwrap(); 151 | assert_eq!(root_map.len(), 2); 152 | 153 | let nested_0 = root_map 154 | .get_item("nested_0") 155 | .unwrap() 156 | .unwrap() 157 | .cast_into::() 158 | .unwrap(); 159 | assert_eq!(nested_0.len(), 1); 160 | let nested_key_0: String = nested_0 161 | .get_item("nested_key") 162 | .unwrap() 163 | .unwrap() 164 | .extract() 165 | .unwrap(); 166 | assert_eq!(nested_key_0, "nested_value_0"); 167 | 168 | let nested_1 = root_map 169 | .get_item("nested_1") 170 | .unwrap() 171 | .unwrap() 172 | .cast_into::() 173 | .unwrap(); 174 | assert_eq!(nested_1.len(), 1); 175 | let nested_key_1: String = nested_1 176 | .get_item("nested_key") 177 | .unwrap() 178 | .unwrap() 179 | .extract() 180 | .unwrap(); 181 | assert_eq!(nested_key_1, "nested_value_1"); 182 | }); 183 | } 184 | 185 | #[test] 186 | fn test_ser_invalid() { 187 | Python::attach(|py| { 188 | let root = Root { 189 | root_key: String::from("root_value"), 190 | root_map: BTreeMap::from([ 191 | ( 192 | String::from("nested_0"), 193 | Nested { 194 | nested_key: CannotSerialize {}, 195 | }, 196 | ), 197 | ( 198 | String::from("nested_1"), 199 | Nested { 200 | nested_key: CannotSerialize {}, 201 | }, 202 | ), 203 | ]), 204 | }; 205 | 206 | let ser = pythonize::Pythonizer::>::from(py); 207 | let err = serde_path_to_error::serialize(&root, ser).unwrap_err(); 208 | 209 | assert_eq!(err.path().to_string(), "root_map.nested_0.nested_key"); 210 | }); 211 | } 212 | -------------------------------------------------------------------------------- /tests/test_custom_types.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use pyo3::{ 4 | exceptions::{PyIndexError, PyKeyError}, 5 | prelude::*, 6 | types::{PyDict, PyMapping, PySequence, PyTuple}, 7 | IntoPyObjectExt, 8 | }; 9 | use pythonize::{ 10 | depythonize, pythonize_custom, PythonizeListType, PythonizeMappingType, 11 | PythonizeNamedMappingType, PythonizeTypes, PythonizeUnnamedMappingAdapter, Pythonizer, 12 | }; 13 | use serde::Serialize; 14 | use serde_json::{json, Value}; 15 | 16 | #[pyclass(sequence)] 17 | struct CustomList { 18 | items: Vec>, 19 | } 20 | 21 | #[pymethods] 22 | impl CustomList { 23 | fn __len__(&self) -> usize { 24 | self.items.len() 25 | } 26 | 27 | fn __getitem__(&self, idx: isize) -> PyResult> { 28 | self.items 29 | .get(idx as usize) 30 | .cloned() 31 | .ok_or_else(|| PyIndexError::new_err(idx)) 32 | } 33 | } 34 | 35 | impl PythonizeListType for CustomList { 36 | fn create_sequence<'py, T, U>( 37 | py: Python<'py>, 38 | elements: impl IntoIterator, 39 | ) -> PyResult> 40 | where 41 | T: IntoPyObject<'py>, 42 | U: ExactSizeIterator, 43 | { 44 | let sequence = Bound::new( 45 | py, 46 | CustomList { 47 | items: elements 48 | .into_iter() 49 | .map(|item| item.into_py_any(py)) 50 | .collect::>()?, 51 | }, 52 | )?; 53 | 54 | Ok(unsafe { sequence.cast_into_unchecked() }) 55 | } 56 | } 57 | 58 | struct PythonizeCustomList; 59 | impl<'py> PythonizeTypes for PythonizeCustomList { 60 | type Map = PyDict; 61 | type NamedMap = PythonizeUnnamedMappingAdapter; 62 | type List = CustomList; 63 | } 64 | 65 | #[test] 66 | fn test_custom_list() { 67 | Python::attach(|py| { 68 | PySequence::register::(py).unwrap(); 69 | let serialized = pythonize_custom::(py, &json!([1, 2, 3])).unwrap(); 70 | assert!(serialized.is_instance_of::()); 71 | 72 | let deserialized: Value = depythonize(&serialized).unwrap(); 73 | assert_eq!(deserialized, json!([1, 2, 3])); 74 | }) 75 | } 76 | 77 | #[pyclass(mapping)] 78 | struct CustomDict { 79 | items: HashMap>, 80 | } 81 | 82 | #[pymethods] 83 | impl CustomDict { 84 | fn __len__(&self) -> usize { 85 | self.items.len() 86 | } 87 | 88 | fn __getitem__(&self, key: String) -> PyResult> { 89 | self.items 90 | .get(&key) 91 | .cloned() 92 | .ok_or_else(|| PyKeyError::new_err(key)) 93 | } 94 | 95 | fn __setitem__(&mut self, key: String, value: Py) { 96 | self.items.insert(key, value); 97 | } 98 | 99 | fn keys(&self) -> Vec<&String> { 100 | self.items.keys().collect() 101 | } 102 | 103 | fn values(&self) -> Vec> { 104 | self.items.values().cloned().collect() 105 | } 106 | } 107 | 108 | impl PythonizeMappingType for CustomDict { 109 | type Builder<'py> = Bound<'py, CustomDict>; 110 | 111 | fn builder<'py>(py: Python<'py>, len: Option) -> PyResult> { 112 | Bound::new( 113 | py, 114 | CustomDict { 115 | items: HashMap::with_capacity(len.unwrap_or(0)), 116 | }, 117 | ) 118 | } 119 | 120 | fn push_item<'py>( 121 | builder: &mut Self::Builder<'py>, 122 | key: Bound<'py, PyAny>, 123 | value: Bound<'py, PyAny>, 124 | ) -> PyResult<()> { 125 | unsafe { builder.cast_unchecked::() }.set_item(key, value) 126 | } 127 | 128 | fn finish<'py>(builder: Self::Builder<'py>) -> PyResult> { 129 | Ok(unsafe { builder.cast_into_unchecked() }) 130 | } 131 | } 132 | 133 | struct PythonizeCustomDict; 134 | impl<'py> PythonizeTypes for PythonizeCustomDict { 135 | type Map = CustomDict; 136 | type NamedMap = PythonizeUnnamedMappingAdapter; 137 | type List = PyTuple; 138 | } 139 | 140 | #[test] 141 | fn test_custom_dict() { 142 | Python::attach(|py| { 143 | PyMapping::register::(py).unwrap(); 144 | let serialized = 145 | pythonize_custom::(py, &json!({ "hello": 1, "world": 2 })) 146 | .unwrap(); 147 | assert!(serialized.is_instance_of::()); 148 | 149 | let deserialized: Value = depythonize(&serialized).unwrap(); 150 | assert_eq!(deserialized, json!({ "hello": 1, "world": 2 })); 151 | }) 152 | } 153 | 154 | #[test] 155 | fn test_tuple() { 156 | Python::attach(|py| { 157 | PyMapping::register::(py).unwrap(); 158 | let serialized = 159 | pythonize_custom::(py, &json!([1, 2, 3, 4])).unwrap(); 160 | assert!(serialized.is_instance_of::()); 161 | 162 | let deserialized: Value = depythonize(&serialized).unwrap(); 163 | assert_eq!(deserialized, json!([1, 2, 3, 4])); 164 | }) 165 | } 166 | 167 | #[test] 168 | fn test_pythonizer_can_be_created() { 169 | // https://github.com/davidhewitt/pythonize/pull/56 170 | Python::attach(|py| { 171 | let sample = json!({ "hello": 1, "world": 2 }); 172 | assert!(sample 173 | .serialize(Pythonizer::new(py)) 174 | .unwrap() 175 | .is_instance_of::()); 176 | 177 | assert!(sample 178 | .serialize(Pythonizer::custom::(py)) 179 | .unwrap() 180 | .is_instance_of::()); 181 | }) 182 | } 183 | 184 | #[pyclass(mapping)] 185 | struct NamedCustomDict { 186 | name: String, 187 | items: HashMap>, 188 | } 189 | 190 | #[pymethods] 191 | impl NamedCustomDict { 192 | fn __len__(&self) -> usize { 193 | self.items.len() 194 | } 195 | 196 | fn __getitem__(&self, key: String) -> PyResult> { 197 | self.items 198 | .get(&key) 199 | .cloned() 200 | .ok_or_else(|| PyKeyError::new_err(key)) 201 | } 202 | 203 | fn __setitem__(&mut self, key: String, value: Py) { 204 | self.items.insert(key, value); 205 | } 206 | 207 | fn keys(&self) -> Vec<&String> { 208 | self.items.keys().collect() 209 | } 210 | 211 | fn values(&self) -> Vec> { 212 | self.items.values().cloned().collect() 213 | } 214 | } 215 | 216 | impl PythonizeNamedMappingType for NamedCustomDict { 217 | type Builder<'py> = Bound<'py, NamedCustomDict>; 218 | 219 | fn builder<'py>( 220 | py: Python<'py>, 221 | len: usize, 222 | name: &'static str, 223 | ) -> PyResult> { 224 | Bound::new( 225 | py, 226 | NamedCustomDict { 227 | name: String::from(name), 228 | items: HashMap::with_capacity(len), 229 | }, 230 | ) 231 | } 232 | 233 | fn push_field<'py>( 234 | builder: &mut Self::Builder<'py>, 235 | name: Bound<'py, pyo3::types::PyString>, 236 | value: Bound<'py, PyAny>, 237 | ) -> PyResult<()> { 238 | unsafe { builder.cast_unchecked::() }.set_item(name, value) 239 | } 240 | 241 | fn finish<'py>(builder: Self::Builder<'py>) -> PyResult> { 242 | Ok(unsafe { builder.cast_into_unchecked() }) 243 | } 244 | } 245 | 246 | struct PythonizeNamedCustomDict; 247 | impl<'py> PythonizeTypes for PythonizeNamedCustomDict { 248 | type Map = CustomDict; 249 | type NamedMap = NamedCustomDict; 250 | type List = PyTuple; 251 | } 252 | 253 | #[derive(Serialize)] 254 | struct Struct { 255 | hello: u8, 256 | world: i8, 257 | } 258 | 259 | #[test] 260 | fn test_custom_unnamed_dict() { 261 | Python::attach(|py| { 262 | PyMapping::register::(py).unwrap(); 263 | let serialized = 264 | pythonize_custom::(py, &Struct { hello: 1, world: 2 }).unwrap(); 265 | assert!(serialized.is_instance_of::()); 266 | 267 | let deserialized: Value = depythonize(&serialized).unwrap(); 268 | assert_eq!(deserialized, json!({ "hello": 1, "world": 2 })); 269 | }) 270 | } 271 | 272 | #[test] 273 | fn test_custom_named_dict() { 274 | Python::attach(|py| { 275 | PyMapping::register::(py).unwrap(); 276 | let serialized = 277 | pythonize_custom::(py, &Struct { hello: 1, world: 2 }) 278 | .unwrap(); 279 | let named: Bound = serialized.extract().unwrap(); 280 | assert_eq!(named.borrow().name, "Struct"); 281 | 282 | let deserialized: Value = depythonize(&serialized).unwrap(); 283 | assert_eq!(deserialized, json!({ "hello": 1, "world": 2 })); 284 | }) 285 | } 286 | -------------------------------------------------------------------------------- /src/ser.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use pyo3::types::{ 4 | PyDict, PyDictMethods, PyList, PyListMethods, PyMapping, PySequence, PyString, PyTuple, 5 | PyTupleMethods, 6 | }; 7 | use pyo3::{Bound, BoundObject, IntoPyObject, PyAny, PyResult, Python}; 8 | use serde::{ser, Serialize}; 9 | 10 | use crate::error::{PythonizeError, Result}; 11 | 12 | /// Trait for types which can represent a Python mapping 13 | pub trait PythonizeMappingType { 14 | /// Builder type for Python mappings 15 | type Builder<'py>: 'py; 16 | 17 | /// Create a builder for a Python mapping 18 | fn builder<'py>(py: Python<'py>, len: Option) -> PyResult>; 19 | 20 | /// Adds the key-value item to the mapping being built 21 | fn push_item<'py>( 22 | builder: &mut Self::Builder<'py>, 23 | key: Bound<'py, PyAny>, 24 | value: Bound<'py, PyAny>, 25 | ) -> PyResult<()>; 26 | 27 | /// Build the Python mapping 28 | fn finish<'py>(builder: Self::Builder<'py>) -> PyResult>; 29 | } 30 | 31 | /// Trait for types which can represent a Python mapping and have a name 32 | pub trait PythonizeNamedMappingType { 33 | /// Builder type for Python mappings with a name 34 | type Builder<'py>: 'py; 35 | 36 | /// Create a builder for a Python mapping with a name 37 | fn builder<'py>( 38 | py: Python<'py>, 39 | len: usize, 40 | name: &'static str, 41 | ) -> PyResult>; 42 | 43 | /// Adds the field to the named mapping being built 44 | fn push_field<'py>( 45 | builder: &mut Self::Builder<'py>, 46 | name: Bound<'py, PyString>, 47 | value: Bound<'py, PyAny>, 48 | ) -> PyResult<()>; 49 | 50 | /// Build the Python mapping 51 | fn finish<'py>(builder: Self::Builder<'py>) -> PyResult>; 52 | } 53 | 54 | /// Trait for types which can represent a Python sequence 55 | pub trait PythonizeListType: Sized { 56 | /// Constructor 57 | fn create_sequence<'py, T, U>( 58 | py: Python<'py>, 59 | elements: impl IntoIterator, 60 | ) -> PyResult> 61 | where 62 | T: IntoPyObject<'py>, 63 | U: ExactSizeIterator; 64 | } 65 | 66 | /// Custom types for serialization 67 | pub trait PythonizeTypes { 68 | /// Python map type (should be representable as python mapping) 69 | type Map: PythonizeMappingType; 70 | /// Python (struct-like) named map type (should be representable as python mapping) 71 | type NamedMap: PythonizeNamedMappingType; 72 | /// Python sequence type (should be representable as python sequence) 73 | type List: PythonizeListType; 74 | } 75 | 76 | impl PythonizeMappingType for PyDict { 77 | type Builder<'py> = Bound<'py, Self>; 78 | 79 | fn builder<'py>(py: Python<'py>, _len: Option) -> PyResult> { 80 | Ok(Self::new(py)) 81 | } 82 | 83 | fn push_item<'py>( 84 | builder: &mut Self::Builder<'py>, 85 | key: Bound<'py, PyAny>, 86 | value: Bound<'py, PyAny>, 87 | ) -> PyResult<()> { 88 | builder.set_item(key, value) 89 | } 90 | 91 | fn finish<'py>(builder: Self::Builder<'py>) -> PyResult> { 92 | Ok(builder.into_mapping()) 93 | } 94 | } 95 | 96 | /// Adapter type to use an unnamed mapping type, i.e. one that implements 97 | /// [`PythonizeMappingType`], as a named mapping type, i.e. one that implements 98 | /// [`PythonizeNamedMappingType`]. The adapter simply drops the provided name. 99 | /// 100 | /// This adapter is commonly applied to use the same unnamed mapping type for 101 | /// both [`PythonizeTypes::Map`] and [`PythonizeTypes::NamedMap`] while only 102 | /// implementing [`PythonizeMappingType`]. 103 | pub struct PythonizeUnnamedMappingAdapter { 104 | _unnamed: T, 105 | } 106 | 107 | impl PythonizeNamedMappingType for PythonizeUnnamedMappingAdapter { 108 | type Builder<'py> = T::Builder<'py>; 109 | 110 | fn builder<'py>( 111 | py: Python<'py>, 112 | len: usize, 113 | _name: &'static str, 114 | ) -> PyResult> { 115 | T::builder(py, Some(len)) 116 | } 117 | 118 | fn push_field<'py>( 119 | builder: &mut Self::Builder<'py>, 120 | name: Bound<'py, PyString>, 121 | value: Bound<'py, PyAny>, 122 | ) -> PyResult<()> { 123 | T::push_item(builder, name.into_any(), value) 124 | } 125 | 126 | fn finish<'py>(builder: Self::Builder<'py>) -> PyResult> { 127 | T::finish(builder) 128 | } 129 | } 130 | 131 | impl PythonizeListType for PyList { 132 | fn create_sequence<'py, T, U>( 133 | py: Python<'py>, 134 | elements: impl IntoIterator, 135 | ) -> PyResult> 136 | where 137 | T: IntoPyObject<'py>, 138 | U: ExactSizeIterator, 139 | { 140 | Ok(PyList::new(py, elements)?.into_sequence()) 141 | } 142 | } 143 | 144 | impl PythonizeListType for PyTuple { 145 | fn create_sequence<'py, T, U>( 146 | py: Python<'py>, 147 | elements: impl IntoIterator, 148 | ) -> PyResult> 149 | where 150 | T: IntoPyObject<'py>, 151 | U: ExactSizeIterator, 152 | { 153 | Ok(PyTuple::new(py, elements)?.into_sequence()) 154 | } 155 | } 156 | 157 | pub struct PythonizeDefault; 158 | 159 | impl PythonizeTypes for PythonizeDefault { 160 | type Map = PyDict; 161 | type NamedMap = PythonizeUnnamedMappingAdapter; 162 | type List = PyList; 163 | } 164 | 165 | /// Attempt to convert the given data into a Python object 166 | pub fn pythonize<'py, T>(py: Python<'py>, value: &T) -> Result> 167 | where 168 | T: ?Sized + Serialize, 169 | { 170 | value.serialize(Pythonizer::new(py)) 171 | } 172 | 173 | /// Attempt to convert the given data into a Python object. 174 | /// Also uses custom mapping python class for serialization. 175 | pub fn pythonize_custom<'py, P, T>(py: Python<'py>, value: &T) -> Result> 176 | where 177 | T: ?Sized + Serialize, 178 | P: PythonizeTypes, 179 | { 180 | value.serialize(Pythonizer::custom::

(py)) 181 | } 182 | 183 | /// A structure that serializes Rust values into Python objects 184 | #[derive(Clone, Copy)] 185 | pub struct Pythonizer<'py, P> { 186 | py: Python<'py>, 187 | _types: PhantomData

, 188 | } 189 | 190 | impl<'py, P> From> for Pythonizer<'py, P> { 191 | fn from(py: Python<'py>) -> Self { 192 | Self { 193 | py, 194 | _types: PhantomData, 195 | } 196 | } 197 | } 198 | 199 | impl<'py> Pythonizer<'py, PythonizeDefault> { 200 | /// Creates a serializer to convert data into a Python object using the default mapping class 201 | pub fn new(py: Python<'py>) -> Self { 202 | Self::from(py) 203 | } 204 | 205 | /// Creates a serializer to convert data into a Python object using a custom mapping class 206 | pub fn custom

(py: Python<'py>) -> Pythonizer<'py, P> { 207 | Pythonizer::from(py) 208 | } 209 | } 210 | 211 | #[doc(hidden)] 212 | pub struct PythonCollectionSerializer<'py, P> { 213 | items: Vec>, 214 | py: Python<'py>, 215 | _types: PhantomData

, 216 | } 217 | 218 | #[doc(hidden)] 219 | pub struct PythonTupleVariantSerializer<'py, P> { 220 | name: &'static str, 221 | variant: &'static str, 222 | inner: PythonCollectionSerializer<'py, P>, 223 | } 224 | 225 | #[doc(hidden)] 226 | pub struct PythonStructVariantSerializer<'py, P: PythonizeTypes> { 227 | name: &'static str, 228 | variant: &'static str, 229 | inner: PythonStructDictSerializer<'py, P>, 230 | } 231 | 232 | #[doc(hidden)] 233 | pub struct PythonStructDictSerializer<'py, P: PythonizeTypes> { 234 | py: Python<'py>, 235 | builder: ::Builder<'py>, 236 | _types: PhantomData

, 237 | } 238 | 239 | #[doc(hidden)] 240 | pub struct PythonMapSerializer<'py, P: PythonizeTypes> { 241 | py: Python<'py>, 242 | builder: ::Builder<'py>, 243 | key: Option>, 244 | _types: PhantomData

, 245 | } 246 | 247 | impl<'py, P: PythonizeTypes> Pythonizer<'py, P> { 248 | /// The default implementation for serialisation functions. 249 | #[inline] 250 | fn serialise_default(self, v: T) -> Result> 251 | where 252 | T: IntoPyObject<'py>, 253 | >::Error: Into, 254 | { 255 | v.into_pyobject(self.py) 256 | .map(|x| x.into_any().into_bound()) 257 | .map_err(Into::into) 258 | } 259 | } 260 | 261 | impl<'py, P: PythonizeTypes> ser::Serializer for Pythonizer<'py, P> { 262 | type Ok = Bound<'py, PyAny>; 263 | type Error = PythonizeError; 264 | type SerializeSeq = PythonCollectionSerializer<'py, P>; 265 | type SerializeTuple = PythonCollectionSerializer<'py, P>; 266 | type SerializeTupleStruct = PythonCollectionSerializer<'py, P>; 267 | type SerializeTupleVariant = PythonTupleVariantSerializer<'py, P>; 268 | type SerializeMap = PythonMapSerializer<'py, P>; 269 | type SerializeStruct = PythonStructDictSerializer<'py, P>; 270 | type SerializeStructVariant = PythonStructVariantSerializer<'py, P>; 271 | 272 | fn serialize_bool(self, v: bool) -> Result> { 273 | self.serialise_default(v) 274 | } 275 | 276 | fn serialize_i8(self, v: i8) -> Result> { 277 | self.serialise_default(v) 278 | } 279 | 280 | fn serialize_i16(self, v: i16) -> Result> { 281 | self.serialise_default(v) 282 | } 283 | 284 | fn serialize_i32(self, v: i32) -> Result> { 285 | self.serialise_default(v) 286 | } 287 | 288 | fn serialize_i64(self, v: i64) -> Result> { 289 | self.serialise_default(v) 290 | } 291 | 292 | fn serialize_u8(self, v: u8) -> Result> { 293 | self.serialise_default(v) 294 | } 295 | 296 | fn serialize_u16(self, v: u16) -> Result> { 297 | self.serialise_default(v) 298 | } 299 | 300 | fn serialize_u32(self, v: u32) -> Result> { 301 | self.serialise_default(v) 302 | } 303 | 304 | fn serialize_u64(self, v: u64) -> Result> { 305 | self.serialise_default(v) 306 | } 307 | 308 | fn serialize_f32(self, v: f32) -> Result> { 309 | self.serialise_default(v) 310 | } 311 | 312 | fn serialize_f64(self, v: f64) -> Result> { 313 | self.serialise_default(v) 314 | } 315 | 316 | fn serialize_char(self, v: char) -> Result> { 317 | self.serialize_str(&v.to_string()) 318 | } 319 | 320 | fn serialize_str(self, v: &str) -> Result> { 321 | Ok(PyString::new(self.py, v).into_any()) 322 | } 323 | 324 | fn serialize_bytes(self, v: &[u8]) -> Result> { 325 | self.serialise_default(v) 326 | } 327 | 328 | fn serialize_none(self) -> Result> { 329 | Ok(self.py.None().into_bound(self.py)) 330 | } 331 | 332 | fn serialize_some(self, value: &T) -> Result> 333 | where 334 | T: ?Sized + Serialize, 335 | { 336 | value.serialize(self) 337 | } 338 | 339 | fn serialize_unit(self) -> Result> { 340 | self.serialize_none() 341 | } 342 | 343 | fn serialize_unit_struct(self, _name: &'static str) -> Result> { 344 | self.serialize_none() 345 | } 346 | 347 | fn serialize_unit_variant( 348 | self, 349 | _name: &'static str, 350 | _variant_index: u32, 351 | variant: &'static str, 352 | ) -> Result> { 353 | self.serialize_str(variant) 354 | } 355 | 356 | fn serialize_newtype_struct( 357 | self, 358 | _name: &'static str, 359 | value: &T, 360 | ) -> Result> 361 | where 362 | T: ?Sized + Serialize, 363 | { 364 | value.serialize(self) 365 | } 366 | 367 | fn serialize_newtype_variant( 368 | self, 369 | name: &'static str, 370 | _variant_index: u32, 371 | variant: &'static str, 372 | value: &T, 373 | ) -> Result> 374 | where 375 | T: ?Sized + Serialize, 376 | { 377 | let mut m = P::NamedMap::builder(self.py, 1, name)?; 378 | P::NamedMap::push_field( 379 | &mut m, 380 | PyString::new(self.py, variant), 381 | value.serialize(self)?, 382 | )?; 383 | Ok(P::NamedMap::finish(m)?.into_any()) 384 | } 385 | 386 | fn serialize_seq(self, len: Option) -> Result> { 387 | let items = match len { 388 | Some(len) => Vec::with_capacity(len), 389 | None => Vec::new(), 390 | }; 391 | Ok(PythonCollectionSerializer { 392 | items, 393 | py: self.py, 394 | _types: PhantomData, 395 | }) 396 | } 397 | 398 | fn serialize_tuple(self, len: usize) -> Result> { 399 | Ok(PythonCollectionSerializer { 400 | items: Vec::with_capacity(len), 401 | py: self.py, 402 | _types: PhantomData, 403 | }) 404 | } 405 | 406 | fn serialize_tuple_struct( 407 | self, 408 | _name: &'static str, 409 | len: usize, 410 | ) -> Result> { 411 | self.serialize_tuple(len) 412 | } 413 | 414 | fn serialize_tuple_variant( 415 | self, 416 | name: &'static str, 417 | _variant_index: u32, 418 | variant: &'static str, 419 | len: usize, 420 | ) -> Result> { 421 | let inner = self.serialize_tuple(len)?; 422 | Ok(PythonTupleVariantSerializer { 423 | name, 424 | variant, 425 | inner, 426 | }) 427 | } 428 | 429 | fn serialize_map(self, len: Option) -> Result> { 430 | Ok(PythonMapSerializer { 431 | builder: P::Map::builder(self.py, len)?, 432 | key: None, 433 | py: self.py, 434 | _types: PhantomData, 435 | }) 436 | } 437 | 438 | fn serialize_struct( 439 | self, 440 | name: &'static str, 441 | len: usize, 442 | ) -> Result> { 443 | Ok(PythonStructDictSerializer { 444 | py: self.py, 445 | builder: P::NamedMap::builder(self.py, len, name)?, 446 | _types: PhantomData, 447 | }) 448 | } 449 | 450 | fn serialize_struct_variant( 451 | self, 452 | name: &'static str, 453 | _variant_index: u32, 454 | variant: &'static str, 455 | len: usize, 456 | ) -> Result> { 457 | Ok(PythonStructVariantSerializer { 458 | name, 459 | variant, 460 | inner: PythonStructDictSerializer { 461 | py: self.py, 462 | builder: P::NamedMap::builder(self.py, len, variant)?, 463 | _types: PhantomData, 464 | }, 465 | }) 466 | } 467 | } 468 | 469 | impl<'py, P: PythonizeTypes> ser::SerializeSeq for PythonCollectionSerializer<'py, P> { 470 | type Ok = Bound<'py, PyAny>; 471 | type Error = PythonizeError; 472 | 473 | fn serialize_element(&mut self, value: &T) -> Result<()> 474 | where 475 | T: ?Sized + Serialize, 476 | { 477 | self.items.push(pythonize_custom::(self.py, value)?); 478 | Ok(()) 479 | } 480 | 481 | fn end(self) -> Result> { 482 | let instance = P::List::create_sequence(self.py, self.items)?; 483 | Ok(instance.into_pyobject(self.py)?.into_any()) 484 | } 485 | } 486 | 487 | impl<'py, P: PythonizeTypes> ser::SerializeTuple for PythonCollectionSerializer<'py, P> { 488 | type Ok = Bound<'py, PyAny>; 489 | type Error = PythonizeError; 490 | 491 | fn serialize_element(&mut self, value: &T) -> Result<()> 492 | where 493 | T: ?Sized + Serialize, 494 | { 495 | ser::SerializeSeq::serialize_element(self, value) 496 | } 497 | 498 | fn end(self) -> Result> { 499 | Ok(PyTuple::new(self.py, self.items)?.into_any()) 500 | } 501 | } 502 | 503 | impl<'py, P: PythonizeTypes> ser::SerializeTupleStruct for PythonCollectionSerializer<'py, P> { 504 | type Ok = Bound<'py, PyAny>; 505 | type Error = PythonizeError; 506 | 507 | fn serialize_field(&mut self, value: &T) -> Result<()> 508 | where 509 | T: ?Sized + Serialize, 510 | { 511 | ser::SerializeSeq::serialize_element(self, value) 512 | } 513 | 514 | fn end(self) -> Result> { 515 | ser::SerializeTuple::end(self) 516 | } 517 | } 518 | 519 | impl<'py, P: PythonizeTypes> ser::SerializeTupleVariant for PythonTupleVariantSerializer<'py, P> { 520 | type Ok = Bound<'py, PyAny>; 521 | type Error = PythonizeError; 522 | 523 | fn serialize_field(&mut self, value: &T) -> Result<()> 524 | where 525 | T: ?Sized + Serialize, 526 | { 527 | ser::SerializeSeq::serialize_element(&mut self.inner, value) 528 | } 529 | 530 | fn end(self) -> Result> { 531 | let mut m = P::NamedMap::builder(self.inner.py, 1, self.name)?; 532 | P::NamedMap::push_field( 533 | &mut m, 534 | PyString::new(self.inner.py, self.variant), 535 | ser::SerializeTuple::end(self.inner)?, 536 | )?; 537 | Ok(P::NamedMap::finish(m)?.into_any()) 538 | } 539 | } 540 | 541 | impl<'py, P: PythonizeTypes> ser::SerializeMap for PythonMapSerializer<'py, P> { 542 | type Ok = Bound<'py, PyAny>; 543 | type Error = PythonizeError; 544 | 545 | fn serialize_key(&mut self, key: &T) -> Result<()> 546 | where 547 | T: ?Sized + Serialize, 548 | { 549 | self.key = Some(pythonize_custom::(self.py, key)?); 550 | Ok(()) 551 | } 552 | 553 | fn serialize_value(&mut self, value: &T) -> Result<()> 554 | where 555 | T: ?Sized + Serialize, 556 | { 557 | P::Map::push_item( 558 | &mut self.builder, 559 | self.key 560 | .take() 561 | .expect("serialize_value should always be called after serialize_key"), 562 | pythonize_custom::(self.py, value)?, 563 | )?; 564 | Ok(()) 565 | } 566 | 567 | fn end(self) -> Result> { 568 | Ok(P::Map::finish(self.builder)?.into_any()) 569 | } 570 | } 571 | 572 | impl<'py, P: PythonizeTypes> ser::SerializeStruct for PythonStructDictSerializer<'py, P> { 573 | type Ok = Bound<'py, PyAny>; 574 | type Error = PythonizeError; 575 | 576 | fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<()> 577 | where 578 | T: ?Sized + Serialize, 579 | { 580 | P::NamedMap::push_field( 581 | &mut self.builder, 582 | PyString::new(self.py, key), 583 | pythonize_custom::(self.py, value)?, 584 | )?; 585 | Ok(()) 586 | } 587 | 588 | fn end(self) -> Result> { 589 | Ok(P::NamedMap::finish(self.builder)?.into_any()) 590 | } 591 | } 592 | 593 | impl<'py, P: PythonizeTypes> ser::SerializeStructVariant for PythonStructVariantSerializer<'py, P> { 594 | type Ok = Bound<'py, PyAny>; 595 | type Error = PythonizeError; 596 | 597 | fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<()> 598 | where 599 | T: ?Sized + Serialize, 600 | { 601 | P::NamedMap::push_field( 602 | &mut self.inner.builder, 603 | PyString::new(self.inner.py, key), 604 | pythonize_custom::(self.inner.py, value)?, 605 | )?; 606 | Ok(()) 607 | } 608 | 609 | fn end(self) -> Result> { 610 | let v = P::NamedMap::finish(self.inner.builder)?; 611 | let mut m = P::NamedMap::builder(self.inner.py, 1, self.name)?; 612 | P::NamedMap::push_field( 613 | &mut m, 614 | PyString::new(self.inner.py, self.variant), 615 | v.into_any(), 616 | )?; 617 | Ok(P::NamedMap::finish(m)?.into_any()) 618 | } 619 | } 620 | 621 | #[cfg(test)] 622 | mod test { 623 | use super::pythonize; 624 | use maplit::hashmap; 625 | use pyo3::ffi::c_str; 626 | use pyo3::prelude::*; 627 | use pyo3::pybacked::PyBackedStr; 628 | use pyo3::types::{PyBytes, PyDict}; 629 | use serde::Serialize; 630 | 631 | fn test_ser(src: T, expected: &str) 632 | where 633 | T: Serialize, 634 | { 635 | Python::attach(|py| -> PyResult<()> { 636 | let obj = pythonize(py, &src)?; 637 | 638 | let locals = PyDict::new(py); 639 | locals.set_item("obj", obj)?; 640 | 641 | py.run( 642 | c_str!("import json; result = json.dumps(obj, separators=(',', ':'))"), 643 | None, 644 | Some(&locals), 645 | )?; 646 | let result = locals.get_item("result")?.unwrap(); 647 | let result = result.extract::()?; 648 | 649 | assert_eq!(result, expected); 650 | assert_eq!(serde_json::to_string(&src).unwrap(), expected); 651 | 652 | Ok(()) 653 | }) 654 | .unwrap(); 655 | } 656 | 657 | #[test] 658 | fn test_empty_struct() { 659 | #[derive(Serialize)] 660 | struct Empty; 661 | 662 | test_ser(Empty, "null"); 663 | } 664 | 665 | #[test] 666 | fn test_struct() { 667 | #[derive(Serialize)] 668 | struct Struct { 669 | foo: String, 670 | bar: usize, 671 | } 672 | 673 | test_ser( 674 | Struct { 675 | foo: "foo".to_string(), 676 | bar: 5, 677 | }, 678 | r#"{"foo":"foo","bar":5}"#, 679 | ); 680 | } 681 | 682 | #[test] 683 | fn test_nested_struct() { 684 | #[derive(Serialize)] 685 | struct Foo { 686 | name: String, 687 | bar: Bar, 688 | } 689 | 690 | #[derive(Serialize)] 691 | struct Bar { 692 | name: String, 693 | } 694 | 695 | test_ser( 696 | Foo { 697 | name: "foo".to_string(), 698 | bar: Bar { 699 | name: "bar".to_string(), 700 | }, 701 | }, 702 | r#"{"name":"foo","bar":{"name":"bar"}}"#, 703 | ) 704 | } 705 | 706 | #[test] 707 | fn test_tuple_struct() { 708 | #[derive(Serialize)] 709 | struct TupleStruct(String, usize); 710 | 711 | test_ser(TupleStruct("foo".to_string(), 5), r#"["foo",5]"#); 712 | } 713 | 714 | #[test] 715 | fn test_tuple() { 716 | test_ser(("foo", 5), r#"["foo",5]"#); 717 | } 718 | 719 | #[test] 720 | fn test_vec() { 721 | test_ser(vec![1, 2, 3], r#"[1,2,3]"#); 722 | } 723 | 724 | #[test] 725 | fn test_map() { 726 | test_ser(hashmap! {"foo" => "foo"}, r#"{"foo":"foo"}"#); 727 | } 728 | 729 | #[test] 730 | fn test_enum_unit_variant() { 731 | #[derive(Serialize)] 732 | enum E { 733 | Empty, 734 | } 735 | 736 | test_ser(E::Empty, r#""Empty""#); 737 | } 738 | 739 | #[test] 740 | fn test_enum_tuple_variant() { 741 | #[derive(Serialize)] 742 | enum E { 743 | Tuple(i32, String), 744 | } 745 | 746 | test_ser(E::Tuple(5, "foo".to_string()), r#"{"Tuple":[5,"foo"]}"#); 747 | } 748 | 749 | #[test] 750 | fn test_enum_newtype_variant() { 751 | #[derive(Serialize)] 752 | enum E { 753 | NewType(String), 754 | } 755 | 756 | test_ser(E::NewType("foo".to_string()), r#"{"NewType":"foo"}"#); 757 | } 758 | 759 | #[test] 760 | fn test_enum_struct_variant() { 761 | #[derive(Serialize)] 762 | enum E { 763 | Struct { foo: String, bar: usize }, 764 | } 765 | 766 | test_ser( 767 | E::Struct { 768 | foo: "foo".to_string(), 769 | bar: 5, 770 | }, 771 | r#"{"Struct":{"foo":"foo","bar":5}}"#, 772 | ); 773 | } 774 | 775 | #[test] 776 | fn test_integers() { 777 | #[derive(Serialize)] 778 | struct Integers { 779 | a: i8, 780 | b: i16, 781 | c: i32, 782 | d: i64, 783 | e: u8, 784 | f: u16, 785 | g: u32, 786 | h: u64, 787 | } 788 | 789 | test_ser( 790 | Integers { 791 | a: 1, 792 | b: 2, 793 | c: 3, 794 | d: 4, 795 | e: 5, 796 | f: 6, 797 | g: 7, 798 | h: 8, 799 | }, 800 | r#"{"a":1,"b":2,"c":3,"d":4,"e":5,"f":6,"g":7,"h":8}"#, 801 | ) 802 | } 803 | 804 | #[test] 805 | fn test_floats() { 806 | #[derive(Serialize)] 807 | struct Floats { 808 | a: f32, 809 | b: f64, 810 | } 811 | 812 | test_ser(Floats { a: 1.0, b: 2.0 }, r#"{"a":1.0,"b":2.0}"#) 813 | } 814 | 815 | #[test] 816 | fn test_char() { 817 | #[derive(Serialize)] 818 | struct Char { 819 | a: char, 820 | } 821 | 822 | test_ser(Char { a: 'a' }, r#"{"a":"a"}"#) 823 | } 824 | 825 | #[test] 826 | fn test_bool() { 827 | test_ser(true, "true"); 828 | test_ser(false, "false"); 829 | } 830 | 831 | #[test] 832 | fn test_none() { 833 | #[derive(Serialize)] 834 | struct S; 835 | 836 | test_ser((), "null"); 837 | test_ser(S, "null"); 838 | 839 | test_ser(Some(1), "1"); 840 | test_ser(None::, "null"); 841 | } 842 | 843 | #[test] 844 | fn test_bytes() { 845 | // serde treats &[u8] as a sequence of integers due to lack of specialization 846 | test_ser(b"foo", "[102,111,111]"); 847 | 848 | Python::attach(|py| { 849 | assert!(pythonize(py, serde_bytes::Bytes::new(b"foo")) 850 | .expect("bytes will always serialize successfully") 851 | .eq(&PyBytes::new(py, b"foo")) 852 | .expect("bytes will always compare successfully")); 853 | }); 854 | } 855 | } 856 | -------------------------------------------------------------------------------- /src/de.rs: -------------------------------------------------------------------------------- 1 | use pyo3::{types::*, Bound}; 2 | use serde::de::{self, IntoDeserializer}; 3 | use serde::Deserialize; 4 | 5 | use crate::error::{ErrorImpl, PythonizeError, Result}; 6 | 7 | /// Attempt to convert a Python object to an instance of `T` 8 | pub fn depythonize<'a, 'py, T>(obj: &'a Bound<'py, PyAny>) -> Result 9 | where 10 | T: Deserialize<'a>, 11 | { 12 | T::deserialize(&mut Depythonizer::from_object(obj)) 13 | } 14 | 15 | /// A structure that deserializes Python objects into Rust values 16 | pub struct Depythonizer<'a, 'py> { 17 | input: &'a Bound<'py, PyAny>, 18 | } 19 | 20 | impl<'a, 'py> Depythonizer<'a, 'py> { 21 | /// Create a deserializer from a Python object 22 | pub fn from_object(input: &'a Bound<'py, PyAny>) -> Self { 23 | Depythonizer { input } 24 | } 25 | 26 | fn sequence_access(&self, expected_len: Option) -> Result> { 27 | let seq = self.input.cast::()?; 28 | let len = self.input.len()?; 29 | 30 | match expected_len { 31 | Some(expected) if expected != len => { 32 | Err(PythonizeError::incorrect_sequence_length(expected, len)) 33 | } 34 | _ => Ok(PySequenceAccess::new(seq, len)), 35 | } 36 | } 37 | 38 | fn set_access(&self) -> Result> { 39 | match self.input.cast::() { 40 | Ok(set) => Ok(PySetAsSequence::from_set(set)), 41 | Err(e) => { 42 | if let Ok(f) = self.input.cast::() { 43 | Ok(PySetAsSequence::from_frozenset(f)) 44 | } else { 45 | Err(e.into()) 46 | } 47 | } 48 | } 49 | } 50 | 51 | fn dict_access(&self) -> Result> { 52 | PyMappingAccess::new(self.input.cast()?) 53 | } 54 | 55 | fn deserialize_any_int<'de, V>(&self, int: &Bound<'_, PyInt>, visitor: V) -> Result 56 | where 57 | V: de::Visitor<'de>, 58 | { 59 | if let Ok(x) = int.extract::() { 60 | if let Ok(x) = u8::try_from(x) { 61 | visitor.visit_u8(x) 62 | } else if let Ok(x) = u16::try_from(x) { 63 | visitor.visit_u16(x) 64 | } else if let Ok(x) = u32::try_from(x) { 65 | visitor.visit_u32(x) 66 | } else if let Ok(x) = u64::try_from(x) { 67 | visitor.visit_u64(x) 68 | } else { 69 | visitor.visit_u128(x) 70 | } 71 | } else { 72 | let x: i128 = int.extract()?; 73 | if let Ok(x) = i8::try_from(x) { 74 | visitor.visit_i8(x) 75 | } else if let Ok(x) = i16::try_from(x) { 76 | visitor.visit_i16(x) 77 | } else if let Ok(x) = i32::try_from(x) { 78 | visitor.visit_i32(x) 79 | } else if let Ok(x) = i64::try_from(x) { 80 | visitor.visit_i64(x) 81 | } else { 82 | visitor.visit_i128(x) 83 | } 84 | } 85 | } 86 | } 87 | 88 | macro_rules! deserialize_type { 89 | ($method:ident => $visit:ident) => { 90 | fn $method(self, visitor: V) -> Result 91 | where 92 | V: de::Visitor<'de>, 93 | { 94 | visitor.$visit(self.input.extract()?) 95 | } 96 | }; 97 | } 98 | 99 | impl<'de> de::Deserializer<'de> for &'_ mut Depythonizer<'_, '_> { 100 | type Error = PythonizeError; 101 | 102 | fn deserialize_any(self, visitor: V) -> Result 103 | where 104 | V: de::Visitor<'de>, 105 | { 106 | let obj = self.input; 107 | 108 | // First check for cases which are cheap to check due to pointer 109 | // comparison or bitflag checks 110 | if obj.is_none() { 111 | self.deserialize_unit(visitor) 112 | } else if obj.is_instance_of::() { 113 | self.deserialize_bool(visitor) 114 | } else if let Ok(x) = obj.cast::() { 115 | self.deserialize_any_int(x, visitor) 116 | } else if obj.is_instance_of::() || obj.is_instance_of::() { 117 | self.deserialize_tuple(obj.len()?, visitor) 118 | } else if obj.is_instance_of::() { 119 | self.deserialize_map(visitor) 120 | } else if obj.is_instance_of::() { 121 | self.deserialize_str(visitor) 122 | } 123 | // Continue with cases which are slower to check because they go 124 | // through `isinstance` machinery 125 | else if obj.is_instance_of::() || obj.is_instance_of::() { 126 | self.deserialize_bytes(visitor) 127 | } else if obj.is_instance_of::() { 128 | self.deserialize_f64(visitor) 129 | } else if obj.is_instance_of::() || obj.is_instance_of::() { 130 | self.deserialize_seq(visitor) 131 | } else if obj.cast::().is_ok() { 132 | self.deserialize_tuple(obj.len()?, visitor) 133 | } else if obj.cast::().is_ok() { 134 | self.deserialize_map(visitor) 135 | } else { 136 | Err(obj.get_type().qualname().map_or_else( 137 | |_| PythonizeError::unsupported_type("unknown"), 138 | PythonizeError::unsupported_type, 139 | )) 140 | } 141 | } 142 | 143 | fn deserialize_bool(self, visitor: V) -> Result 144 | where 145 | V: de::Visitor<'de>, 146 | { 147 | visitor.visit_bool(self.input.is_truthy()?) 148 | } 149 | 150 | fn deserialize_char(self, visitor: V) -> Result 151 | where 152 | V: de::Visitor<'de>, 153 | { 154 | let s = self.input.cast::()?.to_cow()?; 155 | if s.len() != 1 { 156 | return Err(PythonizeError::invalid_length_char()); 157 | } 158 | visitor.visit_char(s.chars().next().unwrap()) 159 | } 160 | 161 | deserialize_type!(deserialize_i8 => visit_i8); 162 | deserialize_type!(deserialize_i16 => visit_i16); 163 | deserialize_type!(deserialize_i32 => visit_i32); 164 | deserialize_type!(deserialize_i64 => visit_i64); 165 | deserialize_type!(deserialize_i128 => visit_i128); 166 | deserialize_type!(deserialize_u8 => visit_u8); 167 | deserialize_type!(deserialize_u16 => visit_u16); 168 | deserialize_type!(deserialize_u32 => visit_u32); 169 | deserialize_type!(deserialize_u64 => visit_u64); 170 | deserialize_type!(deserialize_u128 => visit_u128); 171 | deserialize_type!(deserialize_f32 => visit_f32); 172 | deserialize_type!(deserialize_f64 => visit_f64); 173 | 174 | fn deserialize_str(self, visitor: V) -> Result 175 | where 176 | V: de::Visitor<'de>, 177 | { 178 | let s = self.input.cast::()?; 179 | visitor.visit_str(&s.to_cow()?) 180 | } 181 | 182 | fn deserialize_string(self, visitor: V) -> Result 183 | where 184 | V: de::Visitor<'de>, 185 | { 186 | self.deserialize_str(visitor) 187 | } 188 | 189 | fn deserialize_bytes(self, visitor: V) -> Result 190 | where 191 | V: de::Visitor<'de>, 192 | { 193 | let b = self.input.cast::()?; 194 | visitor.visit_bytes(b.as_bytes()) 195 | } 196 | 197 | fn deserialize_byte_buf(self, visitor: V) -> Result 198 | where 199 | V: de::Visitor<'de>, 200 | { 201 | self.deserialize_bytes(visitor) 202 | } 203 | 204 | fn deserialize_option(self, visitor: V) -> Result 205 | where 206 | V: de::Visitor<'de>, 207 | { 208 | if self.input.is_none() { 209 | visitor.visit_none() 210 | } else { 211 | visitor.visit_some(self) 212 | } 213 | } 214 | 215 | fn deserialize_unit(self, visitor: V) -> Result 216 | where 217 | V: de::Visitor<'de>, 218 | { 219 | if self.input.is_none() { 220 | visitor.visit_unit() 221 | } else { 222 | Err(PythonizeError::msg("expected None")) 223 | } 224 | } 225 | 226 | fn deserialize_unit_struct(self, _name: &'static str, visitor: V) -> Result 227 | where 228 | V: de::Visitor<'de>, 229 | { 230 | self.deserialize_unit(visitor) 231 | } 232 | 233 | fn deserialize_newtype_struct(self, _name: &'static str, visitor: V) -> Result 234 | where 235 | V: de::Visitor<'de>, 236 | { 237 | visitor.visit_newtype_struct(self) 238 | } 239 | 240 | fn deserialize_seq(self, visitor: V) -> Result 241 | where 242 | V: de::Visitor<'de>, 243 | { 244 | match self.sequence_access(None) { 245 | Ok(seq) => visitor.visit_seq(seq), 246 | Err(e) => { 247 | // we allow sets to be deserialized as sequences, so try that 248 | if matches!(*e.inner, ErrorImpl::UnexpectedType(_)) { 249 | if let Ok(set) = self.set_access() { 250 | return visitor.visit_seq(set); 251 | } 252 | } 253 | Err(e) 254 | } 255 | } 256 | } 257 | 258 | fn deserialize_tuple(self, len: usize, visitor: V) -> Result 259 | where 260 | V: de::Visitor<'de>, 261 | { 262 | visitor.visit_seq(self.sequence_access(Some(len))?) 263 | } 264 | 265 | fn deserialize_tuple_struct( 266 | self, 267 | _name: &'static str, 268 | len: usize, 269 | visitor: V, 270 | ) -> Result 271 | where 272 | V: de::Visitor<'de>, 273 | { 274 | visitor.visit_seq(self.sequence_access(Some(len))?) 275 | } 276 | 277 | fn deserialize_map(self, visitor: V) -> Result 278 | where 279 | V: de::Visitor<'de>, 280 | { 281 | visitor.visit_map(self.dict_access()?) 282 | } 283 | 284 | fn deserialize_struct( 285 | self, 286 | _name: &'static str, 287 | _fields: &'static [&'static str], 288 | visitor: V, 289 | ) -> Result 290 | where 291 | V: de::Visitor<'de>, 292 | { 293 | self.deserialize_map(visitor) 294 | } 295 | 296 | fn deserialize_enum( 297 | self, 298 | _name: &'static str, 299 | _variants: &'static [&'static str], 300 | visitor: V, 301 | ) -> Result 302 | where 303 | V: de::Visitor<'de>, 304 | { 305 | let item = &self.input; 306 | if let Ok(s) = item.cast::() { 307 | visitor.visit_enum(s.to_cow()?.into_deserializer()) 308 | } else if let Ok(m) = item.cast::() { 309 | // Get the enum variant from the mapping key 310 | if m.len()? != 1 { 311 | return Err(PythonizeError::invalid_length_enum()); 312 | } 313 | let variant: Bound = m 314 | .keys()? 315 | .get_item(0)? 316 | .cast_into::() 317 | .map_err(|_| PythonizeError::dict_key_not_string())?; 318 | let value = m.get_item(&variant)?; 319 | visitor.visit_enum(PyEnumAccess::new(&value, variant)) 320 | } else { 321 | Err(PythonizeError::invalid_enum_type()) 322 | } 323 | } 324 | 325 | fn deserialize_identifier(self, visitor: V) -> Result 326 | where 327 | V: de::Visitor<'de>, 328 | { 329 | let s = self 330 | .input 331 | .cast::() 332 | .map_err(|_| PythonizeError::dict_key_not_string())?; 333 | visitor.visit_str(&s.to_cow()?) 334 | } 335 | 336 | fn deserialize_ignored_any(self, visitor: V) -> Result 337 | where 338 | V: de::Visitor<'de>, 339 | { 340 | visitor.visit_unit() 341 | } 342 | } 343 | 344 | struct PySequenceAccess<'a, 'py> { 345 | seq: &'a Bound<'py, PySequence>, 346 | index: usize, 347 | len: usize, 348 | } 349 | 350 | impl<'a, 'py> PySequenceAccess<'a, 'py> { 351 | fn new(seq: &'a Bound<'py, PySequence>, len: usize) -> Self { 352 | Self { seq, index: 0, len } 353 | } 354 | } 355 | 356 | impl<'de> de::SeqAccess<'de> for PySequenceAccess<'_, '_> { 357 | type Error = PythonizeError; 358 | 359 | fn next_element_seed(&mut self, seed: T) -> Result> 360 | where 361 | T: de::DeserializeSeed<'de>, 362 | { 363 | if self.index < self.len { 364 | let item = self.seq.get_item(self.index)?; 365 | self.index += 1; 366 | seed.deserialize(&mut Depythonizer::from_object(&item)) 367 | .map(Some) 368 | } else { 369 | Ok(None) 370 | } 371 | } 372 | } 373 | 374 | struct PySetAsSequence<'py> { 375 | iter: Bound<'py, PyIterator>, 376 | } 377 | 378 | impl<'py> PySetAsSequence<'py> { 379 | fn from_set(set: &Bound<'py, PySet>) -> Self { 380 | Self { 381 | iter: PyIterator::from_object(set).expect("set is always iterable"), 382 | } 383 | } 384 | 385 | fn from_frozenset(set: &Bound<'py, PyFrozenSet>) -> Self { 386 | Self { 387 | iter: PyIterator::from_object(set).expect("frozenset is always iterable"), 388 | } 389 | } 390 | } 391 | 392 | impl<'de> de::SeqAccess<'de> for PySetAsSequence<'_> { 393 | type Error = PythonizeError; 394 | 395 | fn next_element_seed(&mut self, seed: T) -> Result> 396 | where 397 | T: de::DeserializeSeed<'de>, 398 | { 399 | match self.iter.next() { 400 | Some(item) => seed 401 | .deserialize(&mut Depythonizer::from_object(&item?)) 402 | .map(Some), 403 | None => Ok(None), 404 | } 405 | } 406 | } 407 | 408 | struct PyMappingAccess<'py> { 409 | keys: Bound<'py, PyList>, 410 | values: Bound<'py, PyList>, 411 | key_idx: usize, 412 | val_idx: usize, 413 | len: usize, 414 | } 415 | 416 | impl<'py> PyMappingAccess<'py> { 417 | fn new(map: &Bound<'py, PyMapping>) -> Result { 418 | let keys = map.keys()?; 419 | let values = map.values()?; 420 | let len = map.len()?; 421 | Ok(Self { 422 | keys, 423 | values, 424 | key_idx: 0, 425 | val_idx: 0, 426 | len, 427 | }) 428 | } 429 | } 430 | 431 | impl<'de> de::MapAccess<'de> for PyMappingAccess<'_> { 432 | type Error = PythonizeError; 433 | 434 | fn next_key_seed(&mut self, seed: K) -> Result> 435 | where 436 | K: de::DeserializeSeed<'de>, 437 | { 438 | if self.key_idx < self.len { 439 | let item = self.keys.get_item(self.key_idx)?; 440 | self.key_idx += 1; 441 | seed.deserialize(&mut Depythonizer::from_object(&item)) 442 | .map(Some) 443 | } else { 444 | Ok(None) 445 | } 446 | } 447 | 448 | fn next_value_seed(&mut self, seed: V) -> Result 449 | where 450 | V: de::DeserializeSeed<'de>, 451 | { 452 | let item = self.values.get_item(self.val_idx)?; 453 | self.val_idx += 1; 454 | seed.deserialize(&mut Depythonizer::from_object(&item)) 455 | } 456 | } 457 | 458 | struct PyEnumAccess<'a, 'py> { 459 | de: Depythonizer<'a, 'py>, 460 | variant: Bound<'py, PyString>, 461 | } 462 | 463 | impl<'a, 'py> PyEnumAccess<'a, 'py> { 464 | fn new(obj: &'a Bound<'py, PyAny>, variant: Bound<'py, PyString>) -> Self { 465 | Self { 466 | de: Depythonizer::from_object(obj), 467 | variant, 468 | } 469 | } 470 | } 471 | 472 | impl<'de> de::EnumAccess<'de> for PyEnumAccess<'_, '_> { 473 | type Error = PythonizeError; 474 | type Variant = Self; 475 | 476 | fn variant_seed(self, seed: V) -> Result<(V::Value, Self::Variant)> 477 | where 478 | V: de::DeserializeSeed<'de>, 479 | { 480 | let cow = self.variant.to_cow()?; 481 | let de: de::value::StrDeserializer<'_, PythonizeError> = cow.as_ref().into_deserializer(); 482 | let val = seed.deserialize(de)?; 483 | Ok((val, self)) 484 | } 485 | } 486 | 487 | impl<'de> de::VariantAccess<'de> for PyEnumAccess<'_, '_> { 488 | type Error = PythonizeError; 489 | 490 | fn unit_variant(self) -> Result<()> { 491 | Ok(()) 492 | } 493 | 494 | fn newtype_variant_seed(self, seed: T) -> Result 495 | where 496 | T: de::DeserializeSeed<'de>, 497 | { 498 | seed.deserialize(&mut { self.de }) 499 | } 500 | 501 | fn tuple_variant(self, len: usize, visitor: V) -> Result 502 | where 503 | V: de::Visitor<'de>, 504 | { 505 | visitor.visit_seq(self.de.sequence_access(Some(len))?) 506 | } 507 | 508 | fn struct_variant(self, _fields: &'static [&'static str], visitor: V) -> Result 509 | where 510 | V: de::Visitor<'de>, 511 | { 512 | visitor.visit_map(self.de.dict_access()?) 513 | } 514 | } 515 | 516 | #[cfg(test)] 517 | mod test { 518 | use std::ffi::CStr; 519 | 520 | use super::*; 521 | use crate::error::ErrorImpl; 522 | use maplit::hashmap; 523 | use pyo3::ffi::c_str; 524 | use pyo3::{IntoPyObject, Python}; 525 | use serde_json::{json, Value as JsonValue}; 526 | 527 | fn test_de(code: &CStr, expected: &T, expected_json: &JsonValue) 528 | where 529 | T: de::DeserializeOwned + PartialEq + std::fmt::Debug, 530 | { 531 | Python::attach(|py| { 532 | let obj = py.eval(code, None, None).unwrap(); 533 | let actual: T = depythonize(&obj).unwrap(); 534 | assert_eq!(&actual, expected); 535 | 536 | let actual_json: JsonValue = depythonize(&obj).unwrap(); 537 | assert_eq!(&actual_json, expected_json); 538 | }); 539 | } 540 | 541 | #[test] 542 | fn test_empty_struct() { 543 | #[derive(Debug, Deserialize, PartialEq)] 544 | struct Empty; 545 | 546 | let expected = Empty; 547 | let expected_json = json!(null); 548 | let code = c_str!("None"); 549 | test_de(code, &expected, &expected_json); 550 | } 551 | 552 | #[test] 553 | fn test_struct() { 554 | #[derive(Debug, Deserialize, PartialEq)] 555 | struct Struct { 556 | foo: String, 557 | bar: usize, 558 | baz: f32, 559 | qux: bool, 560 | } 561 | 562 | let expected = Struct { 563 | foo: "Foo".to_string(), 564 | bar: 8usize, 565 | baz: 45.23, 566 | qux: true, 567 | }; 568 | let expected_json = json!({ 569 | "foo": "Foo", 570 | "bar": 8, 571 | "baz": 45.23, 572 | "qux": true 573 | }); 574 | let code = c_str!("{'foo': 'Foo', 'bar': 8, 'baz': 45.23, 'qux': True}"); 575 | test_de(code, &expected, &expected_json); 576 | } 577 | 578 | #[test] 579 | fn test_struct_missing_key() { 580 | #[derive(Debug, Deserialize, PartialEq)] 581 | struct Struct { 582 | foo: String, 583 | bar: usize, 584 | } 585 | 586 | let code = c_str!("{'foo': 'Foo'}"); 587 | 588 | Python::attach(|py| { 589 | let locals = PyDict::new(py); 590 | let obj = py.eval(code, None, Some(&locals)).unwrap(); 591 | assert!(matches!( 592 | *depythonize::(&obj).unwrap_err().inner, 593 | ErrorImpl::Message(msg) if msg == "missing field `bar`" 594 | )); 595 | }) 596 | } 597 | 598 | #[test] 599 | fn test_tuple_struct() { 600 | #[derive(Debug, Deserialize, PartialEq)] 601 | struct TupleStruct(String, f64); 602 | 603 | let expected = TupleStruct("cat".to_string(), -10.05); 604 | let expected_json = json!(["cat", -10.05]); 605 | let code = c_str!("('cat', -10.05)"); 606 | test_de(code, &expected, &expected_json); 607 | } 608 | 609 | #[test] 610 | fn test_tuple_too_long() { 611 | #[derive(Debug, Deserialize, PartialEq)] 612 | struct TupleStruct(String, f64); 613 | 614 | let code = c_str!("('cat', -10.05, 'foo')"); 615 | 616 | Python::attach(|py| { 617 | let locals = PyDict::new(py); 618 | let obj = py.eval(code, None, Some(&locals)).unwrap(); 619 | assert!(matches!( 620 | *depythonize::(&obj).unwrap_err().inner, 621 | ErrorImpl::IncorrectSequenceLength { expected, got } if expected == 2 && got == 3 622 | )); 623 | }) 624 | } 625 | 626 | #[test] 627 | fn test_tuple_struct_from_pylist() { 628 | #[derive(Debug, Deserialize, PartialEq)] 629 | struct TupleStruct(String, f64); 630 | 631 | let expected = TupleStruct("cat".to_string(), -10.05); 632 | let expected_json = json!(["cat", -10.05]); 633 | let code = c_str!("['cat', -10.05]"); 634 | test_de(code, &expected, &expected_json); 635 | } 636 | 637 | #[test] 638 | fn test_tuple() { 639 | let expected = ("foo".to_string(), 5); 640 | let expected_json = json!(["foo", 5]); 641 | let code = c_str!("('foo', 5)"); 642 | test_de(code, &expected, &expected_json); 643 | } 644 | 645 | #[test] 646 | fn test_tuple_from_pylist() { 647 | let expected = ("foo".to_string(), 5); 648 | let expected_json = json!(["foo", 5]); 649 | let code = c_str!("['foo', 5]"); 650 | test_de(code, &expected, &expected_json); 651 | } 652 | 653 | #[test] 654 | fn test_vec_from_pyset() { 655 | let expected = vec!["foo".to_string()]; 656 | let expected_json = json!(["foo"]); 657 | let code = c_str!("{'foo'}"); 658 | test_de(code, &expected, &expected_json); 659 | } 660 | 661 | #[test] 662 | fn test_vec_from_pyfrozenset() { 663 | let expected = vec!["foo".to_string()]; 664 | let expected_json = json!(["foo"]); 665 | let code = c_str!("frozenset({'foo'})"); 666 | test_de(code, &expected, &expected_json); 667 | } 668 | 669 | #[test] 670 | fn test_vec() { 671 | let expected = vec![3, 2, 1]; 672 | let expected_json = json!([3, 2, 1]); 673 | let code = c_str!("[3, 2, 1]"); 674 | test_de(code, &expected, &expected_json); 675 | } 676 | 677 | #[test] 678 | fn test_vec_from_tuple() { 679 | let expected = vec![3, 2, 1]; 680 | let expected_json = json!([3, 2, 1]); 681 | let code = c_str!("(3, 2, 1)"); 682 | test_de(code, &expected, &expected_json); 683 | } 684 | 685 | #[test] 686 | fn test_hashmap() { 687 | let expected = hashmap! {"foo".to_string() => 4}; 688 | let expected_json = json!({"foo": 4 }); 689 | let code = c_str!("{'foo': 4}"); 690 | test_de(code, &expected, &expected_json); 691 | } 692 | 693 | #[test] 694 | fn test_enum_variant() { 695 | #[derive(Debug, Deserialize, PartialEq)] 696 | enum Foo { 697 | Variant, 698 | } 699 | 700 | let expected = Foo::Variant; 701 | let expected_json = json!("Variant"); 702 | let code = c_str!("'Variant'"); 703 | test_de(code, &expected, &expected_json); 704 | } 705 | 706 | #[test] 707 | fn test_enum_tuple_variant() { 708 | #[derive(Debug, Deserialize, PartialEq)] 709 | enum Foo { 710 | Tuple(i32, String), 711 | } 712 | 713 | let expected = Foo::Tuple(12, "cat".to_string()); 714 | let expected_json = json!({"Tuple": [12, "cat"]}); 715 | let code = c_str!("{'Tuple': [12, 'cat']}"); 716 | test_de(code, &expected, &expected_json); 717 | } 718 | 719 | #[test] 720 | fn test_enum_newtype_variant() { 721 | #[derive(Debug, Deserialize, PartialEq)] 722 | enum Foo { 723 | NewType(String), 724 | } 725 | 726 | let expected = Foo::NewType("cat".to_string()); 727 | let expected_json = json!({"NewType": "cat" }); 728 | let code = c_str!("{'NewType': 'cat'}"); 729 | test_de(code, &expected, &expected_json); 730 | } 731 | 732 | #[test] 733 | fn test_enum_struct_variant() { 734 | #[derive(Debug, Deserialize, PartialEq)] 735 | enum Foo { 736 | Struct { foo: String, bar: usize }, 737 | } 738 | 739 | let expected = Foo::Struct { 740 | foo: "cat".to_string(), 741 | bar: 25, 742 | }; 743 | let expected_json = json!({"Struct": {"foo": "cat", "bar": 25 }}); 744 | let code = c_str!("{'Struct': {'foo': 'cat', 'bar': 25}}"); 745 | test_de(code, &expected, &expected_json); 746 | } 747 | #[test] 748 | fn test_enum_untagged_tuple_variant() { 749 | #[derive(Debug, Deserialize, PartialEq)] 750 | #[serde(untagged)] 751 | enum Foo { 752 | Tuple(f32, char), 753 | } 754 | 755 | let expected = Foo::Tuple(12.0, 'c'); 756 | let expected_json = json!([12.0, 'c']); 757 | let code = c_str!("[12.0, 'c']"); 758 | test_de(code, &expected, &expected_json); 759 | } 760 | 761 | #[test] 762 | fn test_enum_untagged_newtype_variant() { 763 | #[derive(Debug, Deserialize, PartialEq)] 764 | #[serde(untagged)] 765 | enum Foo { 766 | NewType(String), 767 | } 768 | 769 | let expected = Foo::NewType("cat".to_string()); 770 | let expected_json = json!("cat"); 771 | let code = c_str!("'cat'"); 772 | test_de(code, &expected, &expected_json); 773 | } 774 | 775 | #[test] 776 | fn test_enum_untagged_struct_variant() { 777 | #[derive(Debug, Deserialize, PartialEq)] 778 | #[serde(untagged)] 779 | enum Foo { 780 | Struct { foo: Vec, bar: [u8; 4] }, 781 | } 782 | 783 | let expected = Foo::Struct { 784 | foo: vec!['a', 'b', 'c'], 785 | bar: [2, 5, 3, 1], 786 | }; 787 | let expected_json = json!({"foo": ["a", "b", "c"], "bar": [2, 5, 3, 1]}); 788 | let code = c_str!("{'foo': ['a', 'b', 'c'], 'bar': [2, 5, 3, 1]}"); 789 | test_de(code, &expected, &expected_json); 790 | } 791 | 792 | #[test] 793 | fn test_nested_type() { 794 | #[derive(Debug, Deserialize, PartialEq)] 795 | struct Foo { 796 | name: String, 797 | bar: Bar, 798 | } 799 | 800 | #[derive(Debug, Deserialize, PartialEq)] 801 | struct Bar { 802 | value: usize, 803 | variant: Baz, 804 | } 805 | 806 | #[derive(Debug, Deserialize, PartialEq)] 807 | enum Baz { 808 | Basic, 809 | Tuple(f32, u32), 810 | } 811 | 812 | let expected = Foo { 813 | name: "SomeFoo".to_string(), 814 | bar: Bar { 815 | value: 13, 816 | variant: Baz::Tuple(-1.5, 8), 817 | }, 818 | }; 819 | let expected_json = 820 | json!({"name": "SomeFoo", "bar": { "value": 13, "variant": { "Tuple": [-1.5, 8]}}}); 821 | let code = 822 | c_str!("{'name': 'SomeFoo', 'bar': {'value': 13, 'variant': {'Tuple': [-1.5, 8]}}}"); 823 | test_de(code, &expected, &expected_json); 824 | } 825 | 826 | #[test] 827 | fn test_int_limits() { 828 | Python::attach(|py| { 829 | // serde_json::Value supports u64 and i64 as maximum sizes 830 | let _: serde_json::Value = depythonize(&u8::MAX.into_pyobject(py).unwrap()).unwrap(); 831 | let _: serde_json::Value = depythonize(&u8::MIN.into_pyobject(py).unwrap()).unwrap(); 832 | let _: serde_json::Value = depythonize(&i8::MAX.into_pyobject(py).unwrap()).unwrap(); 833 | let _: serde_json::Value = depythonize(&i8::MIN.into_pyobject(py).unwrap()).unwrap(); 834 | 835 | let _: serde_json::Value = depythonize(&u16::MAX.into_pyobject(py).unwrap()).unwrap(); 836 | let _: serde_json::Value = depythonize(&u16::MIN.into_pyobject(py).unwrap()).unwrap(); 837 | let _: serde_json::Value = depythonize(&i16::MAX.into_pyobject(py).unwrap()).unwrap(); 838 | let _: serde_json::Value = depythonize(&i16::MIN.into_pyobject(py).unwrap()).unwrap(); 839 | 840 | let _: serde_json::Value = depythonize(&u32::MAX.into_pyobject(py).unwrap()).unwrap(); 841 | let _: serde_json::Value = depythonize(&u32::MIN.into_pyobject(py).unwrap()).unwrap(); 842 | let _: serde_json::Value = depythonize(&i32::MAX.into_pyobject(py).unwrap()).unwrap(); 843 | let _: serde_json::Value = depythonize(&i32::MIN.into_pyobject(py).unwrap()).unwrap(); 844 | 845 | let _: serde_json::Value = depythonize(&u64::MAX.into_pyobject(py).unwrap()).unwrap(); 846 | let _: serde_json::Value = depythonize(&u64::MIN.into_pyobject(py).unwrap()).unwrap(); 847 | let _: serde_json::Value = depythonize(&i64::MAX.into_pyobject(py).unwrap()).unwrap(); 848 | let _: serde_json::Value = depythonize(&i64::MIN.into_pyobject(py).unwrap()).unwrap(); 849 | 850 | let _: u128 = depythonize(&u128::MAX.into_pyobject(py).unwrap()).unwrap(); 851 | let _: i128 = depythonize(&u128::MIN.into_pyobject(py).unwrap()).unwrap(); 852 | 853 | let _: i128 = depythonize(&i128::MAX.into_pyobject(py).unwrap()).unwrap(); 854 | let _: i128 = depythonize(&i128::MIN.into_pyobject(py).unwrap()).unwrap(); 855 | }); 856 | } 857 | 858 | #[test] 859 | fn test_deserialize_bytes() { 860 | Python::attach(|py| { 861 | let obj = PyBytes::new(py, "hello".as_bytes()); 862 | let actual: Vec = depythonize(&obj).unwrap(); 863 | assert_eq!(actual, b"hello"); 864 | }) 865 | } 866 | 867 | #[test] 868 | fn test_char() { 869 | let expected = 'a'; 870 | let expected_json = json!("a"); 871 | let code = c_str!("'a'"); 872 | test_de(code, &expected, &expected_json); 873 | } 874 | 875 | #[test] 876 | fn test_unknown_type() { 877 | Python::attach(|py| { 878 | let obj = py 879 | .import("decimal") 880 | .unwrap() 881 | .getattr("Decimal") 882 | .unwrap() 883 | .call0() 884 | .unwrap(); 885 | let err = depythonize::(&obj).unwrap_err(); 886 | assert!(matches!( 887 | *err.inner, 888 | ErrorImpl::UnsupportedType(name) if name == "Decimal" 889 | )); 890 | }); 891 | } 892 | } 893 | --------------------------------------------------------------------------------