pythonize-0.27.0/.cargo_vcs_info.json0000644000000001360000000000100131530ustar { "git": { "sha1": "43c714f13d40db35b080659c67b818758cc8c202" }, "path_in_vcs": "" }pythonize-0.27.0/.github/FUNDING.yml000064400000000000000000000000241046102023000151140ustar 00000000000000github: davidhewitt pythonize-0.27.0/.github/dependabot.yml000064400000000000000000000011331046102023000161310ustar 00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "cargo" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" pythonize-0.27.0/.github/workflows/ci.yml000064400000000000000000000063451046102023000164660ustar 00000000000000name: CI on: push: branches: - main pull_request: env: CARGO_TERM_COLOR: always jobs: resolve: runs-on: ubuntu-latest outputs: MSRV: ${{ steps.resolve-msrv.outputs.MSRV }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - name: resolve MSRV id: resolve-msrv run: echo MSRV=`python -c 'import tomllib; print(tomllib.load(open("Cargo.toml", "rb"))["package"]["rust-version"])'` >> $GITHUB_OUTPUT fmt: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt - name: Check rust formatting (rustfmt) run: cargo fmt --all -- --check clippy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: clippy - run: cargo clippy --all build: needs: [resolve, fmt] # don't wait for clippy as fails rarely and takes longer name: python${{ matrix.python-version }} ${{ matrix.os }} rust-${{ matrix.rust}} runs-on: ${{ matrix.os }} strategy: fail-fast: false # If one platform fails, allow the rest to keep testing. matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] os: ["macos-latest", "ubuntu-latest", "windows-latest"] rust: [stable] include: - python-version: "3.14" os: "ubuntu-latest" rust: ${{ needs.resolve.outputs.MSRV }} - python-version: "3.14" os: "macos-15-intel" rust: "stable" - python-version: "3.14" os: "ubuntu-24.04-arm" rust: "stable" - python-version: "3.14" os: "windows-11-arm" rust: "stable" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Rust toolchain uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} - uses: Swatinem/rust-cache@v2 continue-on-error: true - if: ${{ matrix.rust == needs.resolve.outputs.MSRV }} name: Set dependencies on MSRV run: cargo +stable update env: CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS: fallback - name: Test run: cargo test --verbose - name: Test (abi3) run: cargo test --verbose --features pyo3/abi3-py37 env: RUST_BACKTRACE: 1 coverage: needs: [fmt] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 continue-on-error: true - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - uses: dtolnay/rust-toolchain@stable with: components: llvm-tools-preview - run: | cargo llvm-cov clean cargo llvm-cov --codecov --output-path codecov.json - uses: codecov/codecov-action@v4 with: file: codecov.json token: ${{ secrets.CODECOV_TOKEN }} pythonize-0.27.0/.github/workflows/release.yml000064400000000000000000000012661046102023000175100ustar 00000000000000name: Release Rust Crate on: push: tags: - "v*" workflow_dispatch: inputs: version: description: The version to build jobs: release: permissions: id-token: write runs-on: ubuntu-latest environment: release steps: - uses: actions/checkout@v5 with: # The tag to build or the tag received by the tag event ref: ${{ github.event.inputs.version || github.ref }} persist-credentials: false - uses: rust-lang/crates-io-auth-action@v1 id: auth - name: Publish to crates.io run: cargo publish env: CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} pythonize-0.27.0/.gitignore000064400000000000000000000000311046102023000137250ustar 00000000000000/target Cargo.lock .idea pythonize-0.27.0/CHANGELOG.md000064400000000000000000000051521046102023000135570ustar 00000000000000## 0.27.0 - 2025-11-07 - Update to PyO3 0.27 ## 0.26.0 - 2025-08-30 ### Packaging - Bump MSRV to 1.74 - Update to PyO3 0.26 ### Changed - `PythonizeTypes`, `PythonizeMappingType` and `PythonizeNamedMappingType` no longer have a lifetime on the trait, instead the `Builder` type is a GAT. ## 0.25.0 - 2025-05-23 ### Packaging - Update to PyO3 0.25 ## 0.24.0 - 2025-03-26 ### Packaging - Update to PyO3 0.24 ## Removed - Remove deprecated `depythonize_bound()` ## 0.23.0 - 2024-11-22 ### Packaging - Update to PyO3 0.23 ## 0.22.0 - 2024-08-10 ### Packaging - Bump MSRV to 1.63 - Update to PyO3 0.22 ### Added - Support `u128` / `i128` integers. - Implement `PythonizeListType` for `PyTuple` - Support deserializing enums from any `PyMapping` instead of just `PyDict` - Support serializing struct-like types to named mappings using `PythonizeTypes::NamedMap` ### Changed - `pythonize()` now returns `Bound<'py, PyAny>` instead of `Py` - `depythonize()` now take `&'a Bound` and is no longer deprecated - `depythonize_bound()` is now deprecated - `Depythonizer::from_object()` now takes `&'a Bound` and is no longer deprecated - `Depythonizer` now contains `&'a Bound` and so has an extra lifetime `'a` ### Removed - Remove support for PyO3's `gil-refs` feature ### Fixed - Fix overflow error attempting to depythonize `u64` values greater than `i64::MAX` to types like `serde_json::Value` - Fix deserializing `set` and `frozenset` into Rust homogeneous containers ## 0.21.1 - 2024-04-02 - Fix compile error when using PyO3 `abi3` feature targeting a minimum version below 3.10 ## 0.21.0 - 2024-04-01 - Bump edition to 2021 - Bump MSRV to 1.56 - Update to PyO3 0.21 - Export `PythonizeDefault` ## 0.20.0 - 2023-10-15 - Update to PyO3 0.20 ## 0.19.0 - 2023-06-11 - Update to PyO3 0.19 ## 0.18.0 - 2023-01-22 - Add LICENSE file to the crate - Update to PyO3 0.18 ## 0.17.0 - 2022-08-24 - Update to PyO3 0.17 ## 0.16.0 - 2022-03-06 - Update to PyO3 0.16 ## 0.15.0 - 2021-11-12 - Update to PyO3 0.15 - Add `pythonize_custom` for customizing the Python types to serialize to. - Add support for `depythonize` to handle arbitrary Python sequence and mapping types. ## 0.14.0 - 2021-07-05 - Update to PyO3 0.14 ## 0.13.0 - 2020-12-28 - Update to PyO3 0.13 ## 0.12.1 - 2020-12-08 - Require `std` feature of `serde`. - Reduce memory consumption when deserializing sequences. - Fix deserializing untagged struct enum variants. - Fix deserializing sequences from Python tuples. ## 0.12.0 - 2020-11-22 - Change release versioning to match `pyo3` major/minor version. - Implement `depythonizer` ## 0.1.0 - 2020-08-12 - Initial release pythonize-0.27.0/Cargo.lock0000644000000152140000000000100111310ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "indoc" version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" dependencies = [ "rustversion", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "libc" version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "maplit" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memoffset" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "portable-atomic" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "proc-macro2" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "pyo3" version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37a6df7eab65fc7bee654a421404947e10a0f7085b6951bf2ea395f4659fb0cf" dependencies = [ "indoc", "libc", "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", "unindent", ] [[package]] name = "pyo3-build-config" version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f77d387774f6f6eec64a004eac0ed525aab7fa1966d94b42f743797b3e395afb" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dd13844a4242793e02df3e2ec093f540d948299a6a77ea9ce7afd8623f542be" dependencies = [ "libc", "pyo3-build-config", ] [[package]] name = "pyo3-macros" version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaf8f9f1108270b90d3676b8679586385430e5c0bb78bb5f043f95499c821a71" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", "syn", ] [[package]] name = "pyo3-macros-backend" version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70a3b2274450ba5288bc9b8c1b69ff569d1d61189d4bff38f8d22e03d17f932b" dependencies = [ "heck", "proc-macro2", "pyo3-build-config", "quote", "syn", ] [[package]] name = "pythonize" version = "0.27.0" dependencies = [ "maplit", "pyo3", "serde", "serde_bytes", "serde_json", "serde_path_to_error", ] [[package]] name = "quote" version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde_bytes" version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" dependencies = [ "serde", "serde_core", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", "serde_core", ] [[package]] name = "serde_path_to_error" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", "serde_core", ] [[package]] name = "syn" version = "2.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "target-lexicon" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unindent" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" pythonize-0.27.0/Cargo.toml0000644000000034100000000000100111470ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.74" name = "pythonize" version = "0.27.0" authors = ["David Hewitt <1939362+davidhewitt@users.noreply.github.com>"] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Serde Serializer & Deserializer from Rust <--> Python, backed by PyO3." homepage = "https://github.com/davidhewitt/pythonize" documentation = "https://docs.rs/crate/pythonize/" readme = "README.md" license = "MIT" repository = "https://github.com/davidhewitt/pythonize" [lib] name = "pythonize" path = "src/lib.rs" [[test]] name = "test_custom_types" path = "tests/test_custom_types.rs" [[test]] name = "test_with_serde_path_to_err" path = "tests/test_with_serde_path_to_err.rs" [dependencies.pyo3] version = "0.27" default-features = false [dependencies.serde] version = "1.0" features = ["std"] default-features = false [dev-dependencies.maplit] version = "1.0.2" [dev-dependencies.pyo3] version = "0.27" features = [ "auto-initialize", "macros", "py-clone", ] default-features = false [dev-dependencies.serde] version = "1.0" features = ["derive"] default-features = false [dev-dependencies.serde_bytes] version = "0.11" [dev-dependencies.serde_json] version = "1.0" [dev-dependencies.serde_path_to_error] version = "0.1.15" pythonize-0.27.0/Cargo.toml.orig000064400000000000000000000015371046102023000146400ustar 00000000000000[package] name = "pythonize" version = "0.27.0" authors = ["David Hewitt <1939362+davidhewitt@users.noreply.github.com>"] edition = "2021" rust-version = "1.74" license = "MIT" description = "Serde Serializer & Deserializer from Rust <--> Python, backed by PyO3." homepage = "https://github.com/davidhewitt/pythonize" repository = "https://github.com/davidhewitt/pythonize" documentation = "https://docs.rs/crate/pythonize/" [dependencies] serde = { version = "1.0", default-features = false, features = ["std"] } pyo3 = { version = "0.27", default-features = false } [dev-dependencies] serde = { version = "1.0", default-features = false, features = ["derive"] } pyo3 = { version = "0.27", default-features = false, features = ["auto-initialize", "macros", "py-clone"] } serde_json = "1.0" serde_bytes = "0.11" maplit = "1.0.2" serde_path_to_error = "0.1.15" pythonize-0.27.0/LICENSE000064400000000000000000000020711046102023000127500ustar 00000000000000Copyright (c) 2022-present David Hewitt and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pythonize-0.27.0/README.md000064400000000000000000000024361046102023000132270ustar 00000000000000# Pythonize This is an experimental serializer for Rust's serde ecosystem, which can convert Rust objects to Python values and back. 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 that which is produced directly by `pythonize`. ## Usage This crate converts Rust types which implement the [Serde] serialization traits into Python objects using the [PyO3] library. Pythonize has two main public APIs: `pythonize` and `depythonize`. [Serde]: https://github.com/serde-rs/serde [PyO3]: https://github.com/PyO3/pyo3 # Examples ```rust use serde::{Serialize, Deserialize}; use pyo3::prelude::*; use pythonize::{depythonize, pythonize}; #[derive(Debug, Serialize, Deserialize, PartialEq)] struct Sample { foo: String, bar: Option } let sample = Sample { foo: "Foo".to_string(), bar: None }; Python::attach(|py| { // Rust -> Python let obj = pythonize(py, &sample).unwrap(); assert_eq!("{'foo': 'Foo', 'bar': None}", &format!("{}", obj.repr().unwrap())); // Python -> Rust let new_sample: Sample = depythonize(&obj).unwrap(); assert_eq!(new_sample, sample); }) ``` pythonize-0.27.0/src/de.rs000064400000000000000000000643501046102023000135000ustar 00000000000000use pyo3::{types::*, Bound}; use serde::de::{self, IntoDeserializer}; use serde::Deserialize; use crate::error::{ErrorImpl, PythonizeError, Result}; /// Attempt to convert a Python object to an instance of `T` pub fn depythonize<'a, 'py, T>(obj: &'a Bound<'py, PyAny>) -> Result where T: Deserialize<'a>, { T::deserialize(&mut Depythonizer::from_object(obj)) } /// A structure that deserializes Python objects into Rust values pub struct Depythonizer<'a, 'py> { input: &'a Bound<'py, PyAny>, } impl<'a, 'py> Depythonizer<'a, 'py> { /// Create a deserializer from a Python object pub fn from_object(input: &'a Bound<'py, PyAny>) -> Self { Depythonizer { input } } fn sequence_access(&self, expected_len: Option) -> Result> { let seq = self.input.cast::()?; let len = self.input.len()?; match expected_len { Some(expected) if expected != len => { Err(PythonizeError::incorrect_sequence_length(expected, len)) } _ => Ok(PySequenceAccess::new(seq, len)), } } fn set_access(&self) -> Result> { match self.input.cast::() { Ok(set) => Ok(PySetAsSequence::from_set(set)), Err(e) => { if let Ok(f) = self.input.cast::() { Ok(PySetAsSequence::from_frozenset(f)) } else { Err(e.into()) } } } } fn dict_access(&self) -> Result> { PyMappingAccess::new(self.input.cast()?) } fn deserialize_any_int<'de, V>(&self, int: &Bound<'_, PyInt>, visitor: V) -> Result where V: de::Visitor<'de>, { if let Ok(x) = int.extract::() { if let Ok(x) = u8::try_from(x) { visitor.visit_u8(x) } else if let Ok(x) = u16::try_from(x) { visitor.visit_u16(x) } else if let Ok(x) = u32::try_from(x) { visitor.visit_u32(x) } else if let Ok(x) = u64::try_from(x) { visitor.visit_u64(x) } else { visitor.visit_u128(x) } } else { let x: i128 = int.extract()?; if let Ok(x) = i8::try_from(x) { visitor.visit_i8(x) } else if let Ok(x) = i16::try_from(x) { visitor.visit_i16(x) } else if let Ok(x) = i32::try_from(x) { visitor.visit_i32(x) } else if let Ok(x) = i64::try_from(x) { visitor.visit_i64(x) } else { visitor.visit_i128(x) } } } } macro_rules! deserialize_type { ($method:ident => $visit:ident) => { fn $method(self, visitor: V) -> Result where V: de::Visitor<'de>, { visitor.$visit(self.input.extract()?) } }; } impl<'de> de::Deserializer<'de> for &'_ mut Depythonizer<'_, '_> { type Error = PythonizeError; fn deserialize_any(self, visitor: V) -> Result where V: de::Visitor<'de>, { let obj = self.input; // First check for cases which are cheap to check due to pointer // comparison or bitflag checks if obj.is_none() { self.deserialize_unit(visitor) } else if obj.is_instance_of::() { self.deserialize_bool(visitor) } else if let Ok(x) = obj.cast::() { self.deserialize_any_int(x, visitor) } else if obj.is_instance_of::() || obj.is_instance_of::() { self.deserialize_tuple(obj.len()?, visitor) } else if obj.is_instance_of::() { self.deserialize_map(visitor) } else if obj.is_instance_of::() { self.deserialize_str(visitor) } // Continue with cases which are slower to check because they go // through `isinstance` machinery else if obj.is_instance_of::() || obj.is_instance_of::() { self.deserialize_bytes(visitor) } else if obj.is_instance_of::() { self.deserialize_f64(visitor) } else if obj.is_instance_of::() || obj.is_instance_of::() { self.deserialize_seq(visitor) } else if obj.cast::().is_ok() { self.deserialize_tuple(obj.len()?, visitor) } else if obj.cast::().is_ok() { self.deserialize_map(visitor) } else { Err(obj.get_type().qualname().map_or_else( |_| PythonizeError::unsupported_type("unknown"), PythonizeError::unsupported_type, )) } } fn deserialize_bool(self, visitor: V) -> Result where V: de::Visitor<'de>, { visitor.visit_bool(self.input.is_truthy()?) } fn deserialize_char(self, visitor: V) -> Result where V: de::Visitor<'de>, { let s = self.input.cast::()?.to_cow()?; if s.len() != 1 { return Err(PythonizeError::invalid_length_char()); } visitor.visit_char(s.chars().next().unwrap()) } deserialize_type!(deserialize_i8 => visit_i8); deserialize_type!(deserialize_i16 => visit_i16); deserialize_type!(deserialize_i32 => visit_i32); deserialize_type!(deserialize_i64 => visit_i64); deserialize_type!(deserialize_i128 => visit_i128); deserialize_type!(deserialize_u8 => visit_u8); deserialize_type!(deserialize_u16 => visit_u16); deserialize_type!(deserialize_u32 => visit_u32); deserialize_type!(deserialize_u64 => visit_u64); deserialize_type!(deserialize_u128 => visit_u128); deserialize_type!(deserialize_f32 => visit_f32); deserialize_type!(deserialize_f64 => visit_f64); fn deserialize_str(self, visitor: V) -> Result where V: de::Visitor<'de>, { let s = self.input.cast::()?; visitor.visit_str(&s.to_cow()?) } fn deserialize_string(self, visitor: V) -> Result where V: de::Visitor<'de>, { self.deserialize_str(visitor) } fn deserialize_bytes(self, visitor: V) -> Result where V: de::Visitor<'de>, { let b = self.input.cast::()?; visitor.visit_bytes(b.as_bytes()) } fn deserialize_byte_buf(self, visitor: V) -> Result where V: de::Visitor<'de>, { self.deserialize_bytes(visitor) } fn deserialize_option(self, visitor: V) -> Result where V: de::Visitor<'de>, { if self.input.is_none() { visitor.visit_none() } else { visitor.visit_some(self) } } fn deserialize_unit(self, visitor: V) -> Result where V: de::Visitor<'de>, { if self.input.is_none() { visitor.visit_unit() } else { Err(PythonizeError::msg("expected None")) } } fn deserialize_unit_struct(self, _name: &'static str, visitor: V) -> Result where V: de::Visitor<'de>, { self.deserialize_unit(visitor) } fn deserialize_newtype_struct(self, _name: &'static str, visitor: V) -> Result where V: de::Visitor<'de>, { visitor.visit_newtype_struct(self) } fn deserialize_seq(self, visitor: V) -> Result where V: de::Visitor<'de>, { match self.sequence_access(None) { Ok(seq) => visitor.visit_seq(seq), Err(e) => { // we allow sets to be deserialized as sequences, so try that if matches!(*e.inner, ErrorImpl::UnexpectedType(_)) { if let Ok(set) = self.set_access() { return visitor.visit_seq(set); } } Err(e) } } } fn deserialize_tuple(self, len: usize, visitor: V) -> Result where V: de::Visitor<'de>, { visitor.visit_seq(self.sequence_access(Some(len))?) } fn deserialize_tuple_struct( self, _name: &'static str, len: usize, visitor: V, ) -> Result where V: de::Visitor<'de>, { visitor.visit_seq(self.sequence_access(Some(len))?) } fn deserialize_map(self, visitor: V) -> Result where V: de::Visitor<'de>, { visitor.visit_map(self.dict_access()?) } fn deserialize_struct( self, _name: &'static str, _fields: &'static [&'static str], visitor: V, ) -> Result where V: de::Visitor<'de>, { self.deserialize_map(visitor) } fn deserialize_enum( self, _name: &'static str, _variants: &'static [&'static str], visitor: V, ) -> Result where V: de::Visitor<'de>, { let item = &self.input; if let Ok(s) = item.cast::() { visitor.visit_enum(s.to_cow()?.into_deserializer()) } else if let Ok(m) = item.cast::() { // Get the enum variant from the mapping key if m.len()? != 1 { return Err(PythonizeError::invalid_length_enum()); } let variant: Bound = m .keys()? .get_item(0)? .cast_into::() .map_err(|_| PythonizeError::dict_key_not_string())?; let value = m.get_item(&variant)?; visitor.visit_enum(PyEnumAccess::new(&value, variant)) } else { Err(PythonizeError::invalid_enum_type()) } } fn deserialize_identifier(self, visitor: V) -> Result where V: de::Visitor<'de>, { let s = self .input .cast::() .map_err(|_| PythonizeError::dict_key_not_string())?; visitor.visit_str(&s.to_cow()?) } fn deserialize_ignored_any(self, visitor: V) -> Result where V: de::Visitor<'de>, { visitor.visit_unit() } } struct PySequenceAccess<'a, 'py> { seq: &'a Bound<'py, PySequence>, index: usize, len: usize, } impl<'a, 'py> PySequenceAccess<'a, 'py> { fn new(seq: &'a Bound<'py, PySequence>, len: usize) -> Self { Self { seq, index: 0, len } } } impl<'de> de::SeqAccess<'de> for PySequenceAccess<'_, '_> { type Error = PythonizeError; fn next_element_seed(&mut self, seed: T) -> Result> where T: de::DeserializeSeed<'de>, { if self.index < self.len { let item = self.seq.get_item(self.index)?; self.index += 1; seed.deserialize(&mut Depythonizer::from_object(&item)) .map(Some) } else { Ok(None) } } } struct PySetAsSequence<'py> { iter: Bound<'py, PyIterator>, } impl<'py> PySetAsSequence<'py> { fn from_set(set: &Bound<'py, PySet>) -> Self { Self { iter: PyIterator::from_object(set).expect("set is always iterable"), } } fn from_frozenset(set: &Bound<'py, PyFrozenSet>) -> Self { Self { iter: PyIterator::from_object(set).expect("frozenset is always iterable"), } } } impl<'de> de::SeqAccess<'de> for PySetAsSequence<'_> { type Error = PythonizeError; fn next_element_seed(&mut self, seed: T) -> Result> where T: de::DeserializeSeed<'de>, { match self.iter.next() { Some(item) => seed .deserialize(&mut Depythonizer::from_object(&item?)) .map(Some), None => Ok(None), } } } struct PyMappingAccess<'py> { keys: Bound<'py, PyList>, values: Bound<'py, PyList>, key_idx: usize, val_idx: usize, len: usize, } impl<'py> PyMappingAccess<'py> { fn new(map: &Bound<'py, PyMapping>) -> Result { let keys = map.keys()?; let values = map.values()?; let len = map.len()?; Ok(Self { keys, values, key_idx: 0, val_idx: 0, len, }) } } impl<'de> de::MapAccess<'de> for PyMappingAccess<'_> { type Error = PythonizeError; fn next_key_seed(&mut self, seed: K) -> Result> where K: de::DeserializeSeed<'de>, { if self.key_idx < self.len { let item = self.keys.get_item(self.key_idx)?; self.key_idx += 1; seed.deserialize(&mut Depythonizer::from_object(&item)) .map(Some) } else { Ok(None) } } fn next_value_seed(&mut self, seed: V) -> Result where V: de::DeserializeSeed<'de>, { let item = self.values.get_item(self.val_idx)?; self.val_idx += 1; seed.deserialize(&mut Depythonizer::from_object(&item)) } } struct PyEnumAccess<'a, 'py> { de: Depythonizer<'a, 'py>, variant: Bound<'py, PyString>, } impl<'a, 'py> PyEnumAccess<'a, 'py> { fn new(obj: &'a Bound<'py, PyAny>, variant: Bound<'py, PyString>) -> Self { Self { de: Depythonizer::from_object(obj), variant, } } } impl<'de> de::EnumAccess<'de> for PyEnumAccess<'_, '_> { type Error = PythonizeError; type Variant = Self; fn variant_seed(self, seed: V) -> Result<(V::Value, Self::Variant)> where V: de::DeserializeSeed<'de>, { let cow = self.variant.to_cow()?; let de: de::value::StrDeserializer<'_, PythonizeError> = cow.as_ref().into_deserializer(); let val = seed.deserialize(de)?; Ok((val, self)) } } impl<'de> de::VariantAccess<'de> for PyEnumAccess<'_, '_> { type Error = PythonizeError; fn unit_variant(self) -> Result<()> { Ok(()) } fn newtype_variant_seed(self, seed: T) -> Result where T: de::DeserializeSeed<'de>, { seed.deserialize(&mut { self.de }) } fn tuple_variant(self, len: usize, visitor: V) -> Result where V: de::Visitor<'de>, { visitor.visit_seq(self.de.sequence_access(Some(len))?) } fn struct_variant(self, _fields: &'static [&'static str], visitor: V) -> Result where V: de::Visitor<'de>, { visitor.visit_map(self.de.dict_access()?) } } #[cfg(test)] mod test { use std::ffi::CStr; use super::*; use crate::error::ErrorImpl; use maplit::hashmap; use pyo3::ffi::c_str; use pyo3::{IntoPyObject, Python}; use serde_json::{json, Value as JsonValue}; fn test_de(code: &CStr, expected: &T, expected_json: &JsonValue) where T: de::DeserializeOwned + PartialEq + std::fmt::Debug, { Python::attach(|py| { let obj = py.eval(code, None, None).unwrap(); let actual: T = depythonize(&obj).unwrap(); assert_eq!(&actual, expected); let actual_json: JsonValue = depythonize(&obj).unwrap(); assert_eq!(&actual_json, expected_json); }); } #[test] fn test_empty_struct() { #[derive(Debug, Deserialize, PartialEq)] struct Empty; let expected = Empty; let expected_json = json!(null); let code = c_str!("None"); test_de(code, &expected, &expected_json); } #[test] fn test_struct() { #[derive(Debug, Deserialize, PartialEq)] struct Struct { foo: String, bar: usize, baz: f32, qux: bool, } let expected = Struct { foo: "Foo".to_string(), bar: 8usize, baz: 45.23, qux: true, }; let expected_json = json!({ "foo": "Foo", "bar": 8, "baz": 45.23, "qux": true }); let code = c_str!("{'foo': 'Foo', 'bar': 8, 'baz': 45.23, 'qux': True}"); test_de(code, &expected, &expected_json); } #[test] fn test_struct_missing_key() { #[derive(Debug, Deserialize, PartialEq)] struct Struct { foo: String, bar: usize, } let code = c_str!("{'foo': 'Foo'}"); Python::attach(|py| { let locals = PyDict::new(py); let obj = py.eval(code, None, Some(&locals)).unwrap(); assert!(matches!( *depythonize::(&obj).unwrap_err().inner, ErrorImpl::Message(msg) if msg == "missing field `bar`" )); }) } #[test] fn test_tuple_struct() { #[derive(Debug, Deserialize, PartialEq)] struct TupleStruct(String, f64); let expected = TupleStruct("cat".to_string(), -10.05); let expected_json = json!(["cat", -10.05]); let code = c_str!("('cat', -10.05)"); test_de(code, &expected, &expected_json); } #[test] fn test_tuple_too_long() { #[derive(Debug, Deserialize, PartialEq)] struct TupleStruct(String, f64); let code = c_str!("('cat', -10.05, 'foo')"); Python::attach(|py| { let locals = PyDict::new(py); let obj = py.eval(code, None, Some(&locals)).unwrap(); assert!(matches!( *depythonize::(&obj).unwrap_err().inner, ErrorImpl::IncorrectSequenceLength { expected, got } if expected == 2 && got == 3 )); }) } #[test] fn test_tuple_struct_from_pylist() { #[derive(Debug, Deserialize, PartialEq)] struct TupleStruct(String, f64); let expected = TupleStruct("cat".to_string(), -10.05); let expected_json = json!(["cat", -10.05]); let code = c_str!("['cat', -10.05]"); test_de(code, &expected, &expected_json); } #[test] fn test_tuple() { let expected = ("foo".to_string(), 5); let expected_json = json!(["foo", 5]); let code = c_str!("('foo', 5)"); test_de(code, &expected, &expected_json); } #[test] fn test_tuple_from_pylist() { let expected = ("foo".to_string(), 5); let expected_json = json!(["foo", 5]); let code = c_str!("['foo', 5]"); test_de(code, &expected, &expected_json); } #[test] fn test_vec_from_pyset() { let expected = vec!["foo".to_string()]; let expected_json = json!(["foo"]); let code = c_str!("{'foo'}"); test_de(code, &expected, &expected_json); } #[test] fn test_vec_from_pyfrozenset() { let expected = vec!["foo".to_string()]; let expected_json = json!(["foo"]); let code = c_str!("frozenset({'foo'})"); test_de(code, &expected, &expected_json); } #[test] fn test_vec() { let expected = vec![3, 2, 1]; let expected_json = json!([3, 2, 1]); let code = c_str!("[3, 2, 1]"); test_de(code, &expected, &expected_json); } #[test] fn test_vec_from_tuple() { let expected = vec![3, 2, 1]; let expected_json = json!([3, 2, 1]); let code = c_str!("(3, 2, 1)"); test_de(code, &expected, &expected_json); } #[test] fn test_hashmap() { let expected = hashmap! {"foo".to_string() => 4}; let expected_json = json!({"foo": 4 }); let code = c_str!("{'foo': 4}"); test_de(code, &expected, &expected_json); } #[test] fn test_enum_variant() { #[derive(Debug, Deserialize, PartialEq)] enum Foo { Variant, } let expected = Foo::Variant; let expected_json = json!("Variant"); let code = c_str!("'Variant'"); test_de(code, &expected, &expected_json); } #[test] fn test_enum_tuple_variant() { #[derive(Debug, Deserialize, PartialEq)] enum Foo { Tuple(i32, String), } let expected = Foo::Tuple(12, "cat".to_string()); let expected_json = json!({"Tuple": [12, "cat"]}); let code = c_str!("{'Tuple': [12, 'cat']}"); test_de(code, &expected, &expected_json); } #[test] fn test_enum_newtype_variant() { #[derive(Debug, Deserialize, PartialEq)] enum Foo { NewType(String), } let expected = Foo::NewType("cat".to_string()); let expected_json = json!({"NewType": "cat" }); let code = c_str!("{'NewType': 'cat'}"); test_de(code, &expected, &expected_json); } #[test] fn test_enum_struct_variant() { #[derive(Debug, Deserialize, PartialEq)] enum Foo { Struct { foo: String, bar: usize }, } let expected = Foo::Struct { foo: "cat".to_string(), bar: 25, }; let expected_json = json!({"Struct": {"foo": "cat", "bar": 25 }}); let code = c_str!("{'Struct': {'foo': 'cat', 'bar': 25}}"); test_de(code, &expected, &expected_json); } #[test] fn test_enum_untagged_tuple_variant() { #[derive(Debug, Deserialize, PartialEq)] #[serde(untagged)] enum Foo { Tuple(f32, char), } let expected = Foo::Tuple(12.0, 'c'); let expected_json = json!([12.0, 'c']); let code = c_str!("[12.0, 'c']"); test_de(code, &expected, &expected_json); } #[test] fn test_enum_untagged_newtype_variant() { #[derive(Debug, Deserialize, PartialEq)] #[serde(untagged)] enum Foo { NewType(String), } let expected = Foo::NewType("cat".to_string()); let expected_json = json!("cat"); let code = c_str!("'cat'"); test_de(code, &expected, &expected_json); } #[test] fn test_enum_untagged_struct_variant() { #[derive(Debug, Deserialize, PartialEq)] #[serde(untagged)] enum Foo { Struct { foo: Vec, bar: [u8; 4] }, } let expected = Foo::Struct { foo: vec!['a', 'b', 'c'], bar: [2, 5, 3, 1], }; let expected_json = json!({"foo": ["a", "b", "c"], "bar": [2, 5, 3, 1]}); let code = c_str!("{'foo': ['a', 'b', 'c'], 'bar': [2, 5, 3, 1]}"); test_de(code, &expected, &expected_json); } #[test] fn test_nested_type() { #[derive(Debug, Deserialize, PartialEq)] struct Foo { name: String, bar: Bar, } #[derive(Debug, Deserialize, PartialEq)] struct Bar { value: usize, variant: Baz, } #[derive(Debug, Deserialize, PartialEq)] enum Baz { Basic, Tuple(f32, u32), } let expected = Foo { name: "SomeFoo".to_string(), bar: Bar { value: 13, variant: Baz::Tuple(-1.5, 8), }, }; let expected_json = json!({"name": "SomeFoo", "bar": { "value": 13, "variant": { "Tuple": [-1.5, 8]}}}); let code = c_str!("{'name': 'SomeFoo', 'bar': {'value': 13, 'variant': {'Tuple': [-1.5, 8]}}}"); test_de(code, &expected, &expected_json); } #[test] fn test_int_limits() { Python::attach(|py| { // serde_json::Value supports u64 and i64 as maximum sizes let _: serde_json::Value = depythonize(&u8::MAX.into_pyobject(py).unwrap()).unwrap(); let _: serde_json::Value = depythonize(&u8::MIN.into_pyobject(py).unwrap()).unwrap(); let _: serde_json::Value = depythonize(&i8::MAX.into_pyobject(py).unwrap()).unwrap(); let _: serde_json::Value = depythonize(&i8::MIN.into_pyobject(py).unwrap()).unwrap(); let _: serde_json::Value = depythonize(&u16::MAX.into_pyobject(py).unwrap()).unwrap(); let _: serde_json::Value = depythonize(&u16::MIN.into_pyobject(py).unwrap()).unwrap(); let _: serde_json::Value = depythonize(&i16::MAX.into_pyobject(py).unwrap()).unwrap(); let _: serde_json::Value = depythonize(&i16::MIN.into_pyobject(py).unwrap()).unwrap(); let _: serde_json::Value = depythonize(&u32::MAX.into_pyobject(py).unwrap()).unwrap(); let _: serde_json::Value = depythonize(&u32::MIN.into_pyobject(py).unwrap()).unwrap(); let _: serde_json::Value = depythonize(&i32::MAX.into_pyobject(py).unwrap()).unwrap(); let _: serde_json::Value = depythonize(&i32::MIN.into_pyobject(py).unwrap()).unwrap(); let _: serde_json::Value = depythonize(&u64::MAX.into_pyobject(py).unwrap()).unwrap(); let _: serde_json::Value = depythonize(&u64::MIN.into_pyobject(py).unwrap()).unwrap(); let _: serde_json::Value = depythonize(&i64::MAX.into_pyobject(py).unwrap()).unwrap(); let _: serde_json::Value = depythonize(&i64::MIN.into_pyobject(py).unwrap()).unwrap(); let _: u128 = depythonize(&u128::MAX.into_pyobject(py).unwrap()).unwrap(); let _: i128 = depythonize(&u128::MIN.into_pyobject(py).unwrap()).unwrap(); let _: i128 = depythonize(&i128::MAX.into_pyobject(py).unwrap()).unwrap(); let _: i128 = depythonize(&i128::MIN.into_pyobject(py).unwrap()).unwrap(); }); } #[test] fn test_deserialize_bytes() { Python::attach(|py| { let obj = PyBytes::new(py, "hello".as_bytes()); let actual: Vec = depythonize(&obj).unwrap(); assert_eq!(actual, b"hello"); }) } #[test] fn test_char() { let expected = 'a'; let expected_json = json!("a"); let code = c_str!("'a'"); test_de(code, &expected, &expected_json); } #[test] fn test_unknown_type() { Python::attach(|py| { let obj = py .import("decimal") .unwrap() .getattr("Decimal") .unwrap() .call0() .unwrap(); let err = depythonize::(&obj).unwrap_err(); assert!(matches!( *err.inner, ErrorImpl::UnsupportedType(name) if name == "Decimal" )); }); } } pythonize-0.27.0/src/error.rs000064400000000000000000000132521046102023000142340ustar 00000000000000use pyo3::PyErr; use pyo3::{exceptions::*, CastError, CastIntoError}; use serde::{de, ser}; use std::convert::Infallible; use std::error; use std::fmt::{self, Debug, Display}; use std::result; /// Alias for `std::result::Result` with error type `PythonizeError` pub type Result = result::Result; /// Errors that can occur when serializing/deserializing Python objects pub struct PythonizeError { pub(crate) inner: Box, } impl PythonizeError { pub(crate) fn msg(text: T) -> Self where T: ToString, { Self { inner: Box::new(ErrorImpl::Message(text.to_string())), } } pub(crate) fn unsupported_type(t: T) -> Self where T: ToString, { Self { inner: Box::new(ErrorImpl::UnsupportedType(t.to_string())), } } pub(crate) fn dict_key_not_string() -> Self { Self { inner: Box::new(ErrorImpl::DictKeyNotString), } } pub(crate) fn incorrect_sequence_length(expected: usize, got: usize) -> Self { Self { inner: Box::new(ErrorImpl::IncorrectSequenceLength { expected, got }), } } pub(crate) fn invalid_enum_type() -> Self { Self { inner: Box::new(ErrorImpl::InvalidEnumType), } } pub(crate) fn invalid_length_enum() -> Self { Self { inner: Box::new(ErrorImpl::InvalidLengthEnum), } } pub(crate) fn invalid_length_char() -> Self { Self { inner: Box::new(ErrorImpl::InvalidLengthChar), } } } /// Error codes for problems that can occur when serializing/deserializing Python objects #[derive(Debug)] pub enum ErrorImpl { /// An error originating from the Python runtime PyErr(PyErr), /// Generic error message Message(String), /// A Python type not supported by the deserializer UnsupportedType(String), /// A `PyAny` object that failed to cast to an expected Python type UnexpectedType(String), /// Dict keys should be strings to deserialize to struct fields DictKeyNotString, /// Sequence length did not match expected tuple or tuple struct length. IncorrectSequenceLength { expected: usize, got: usize }, /// Enum variants should either be dict (tagged) or str (variant) InvalidEnumType, /// Tagged enum variants should be a dict with exactly 1 key InvalidLengthEnum, /// Expected a `char`, but got a Python str that was not length 1 InvalidLengthChar, } impl error::Error for PythonizeError {} impl Display for PythonizeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.inner.as_ref() { ErrorImpl::PyErr(e) => Display::fmt(e, f), ErrorImpl::Message(s) => Display::fmt(s, f), ErrorImpl::UnsupportedType(s) => write!(f, "unsupported type {}", s), ErrorImpl::UnexpectedType(s) => write!(f, "unexpected type: {}", s), ErrorImpl::DictKeyNotString => f.write_str("dict keys must have type str"), ErrorImpl::IncorrectSequenceLength { expected, got } => { write!(f, "expected sequence of length {}, got {}", expected, got) } ErrorImpl::InvalidEnumType => f.write_str("expected either a str or dict for enum"), ErrorImpl::InvalidLengthEnum => { f.write_str("expected tagged enum dict to have exactly 1 key") } ErrorImpl::InvalidLengthChar => f.write_str("expected a str of length 1 for char"), } } } impl Debug for PythonizeError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.inner.as_ref().fmt(f) } } impl ser::Error for PythonizeError { fn custom(msg: T) -> Self where T: Display, { Self { inner: Box::new(ErrorImpl::Message(msg.to_string())), } } } impl de::Error for PythonizeError { fn custom(msg: T) -> Self where T: Display, { Self { inner: Box::new(ErrorImpl::Message(msg.to_string())), } } } /// Convert an exception raised in Python to a `PythonizeError` impl From for PythonizeError { fn from(other: Infallible) -> Self { match other {} } } /// Convert an exception raised in Python to a `PythonizeError` impl From for PythonizeError { fn from(other: PyErr) -> Self { Self { inner: Box::new(ErrorImpl::PyErr(other)), } } } /// Handle errors that occur when attempting to use `PyAny::cast` impl<'a, 'py> From> for PythonizeError { fn from(other: CastError<'a, 'py>) -> Self { Self { inner: Box::new(ErrorImpl::UnexpectedType(other.to_string())), } } } /// Handle errors that occur when attempting to use `PyAny::cast` impl<'py> From> for PythonizeError { fn from(other: CastIntoError<'py>) -> Self { Self { inner: Box::new(ErrorImpl::UnexpectedType(other.to_string())), } } } /// Convert a `PythonizeError` to a Python exception impl From for PyErr { fn from(other: PythonizeError) -> Self { match *other.inner { ErrorImpl::PyErr(e) => e, ErrorImpl::Message(e) => PyException::new_err(e), ErrorImpl::UnsupportedType(_) | ErrorImpl::UnexpectedType(_) | ErrorImpl::DictKeyNotString | ErrorImpl::InvalidEnumType => PyTypeError::new_err(other.to_string()), ErrorImpl::IncorrectSequenceLength { .. } | ErrorImpl::InvalidLengthEnum | ErrorImpl::InvalidLengthChar => PyValueError::new_err(other.to_string()), } } } pythonize-0.27.0/src/lib.rs000064400000000000000000000005651046102023000136540ustar 00000000000000#![doc = include_str!("../README.md")] mod de; mod error; mod ser; pub use crate::de::{depythonize, Depythonizer}; pub use crate::error::{PythonizeError, Result}; pub use crate::ser::{ pythonize, pythonize_custom, PythonizeDefault, PythonizeListType, PythonizeMappingType, PythonizeNamedMappingType, PythonizeTypes, PythonizeUnnamedMappingAdapter, Pythonizer, }; pythonize-0.27.0/src/ser.rs000064400000000000000000000547521046102023000137060ustar 00000000000000use std::marker::PhantomData; use pyo3::types::{ PyDict, PyDictMethods, PyList, PyListMethods, PyMapping, PySequence, PyString, PyTuple, PyTupleMethods, }; use pyo3::{Bound, BoundObject, IntoPyObject, PyAny, PyResult, Python}; use serde::{ser, Serialize}; use crate::error::{PythonizeError, Result}; /// Trait for types which can represent a Python mapping pub trait PythonizeMappingType { /// Builder type for Python mappings type Builder<'py>: 'py; /// Create a builder for a Python mapping fn builder<'py>(py: Python<'py>, len: Option) -> PyResult>; /// Adds the key-value item to the mapping being built fn push_item<'py>( builder: &mut Self::Builder<'py>, key: Bound<'py, PyAny>, value: Bound<'py, PyAny>, ) -> PyResult<()>; /// Build the Python mapping fn finish<'py>(builder: Self::Builder<'py>) -> PyResult>; } /// Trait for types which can represent a Python mapping and have a name pub trait PythonizeNamedMappingType { /// Builder type for Python mappings with a name type Builder<'py>: 'py; /// Create a builder for a Python mapping with a name fn builder<'py>( py: Python<'py>, len: usize, name: &'static str, ) -> PyResult>; /// Adds the field to the named mapping being built fn push_field<'py>( builder: &mut Self::Builder<'py>, name: Bound<'py, PyString>, value: Bound<'py, PyAny>, ) -> PyResult<()>; /// Build the Python mapping fn finish<'py>(builder: Self::Builder<'py>) -> PyResult>; } /// Trait for types which can represent a Python sequence pub trait PythonizeListType: Sized { /// Constructor fn create_sequence<'py, T, U>( py: Python<'py>, elements: impl IntoIterator, ) -> PyResult> where T: IntoPyObject<'py>, U: ExactSizeIterator; } /// Custom types for serialization pub trait PythonizeTypes { /// Python map type (should be representable as python mapping) type Map: PythonizeMappingType; /// Python (struct-like) named map type (should be representable as python mapping) type NamedMap: PythonizeNamedMappingType; /// Python sequence type (should be representable as python sequence) type List: PythonizeListType; } impl PythonizeMappingType for PyDict { type Builder<'py> = Bound<'py, Self>; fn builder<'py>(py: Python<'py>, _len: Option) -> PyResult> { Ok(Self::new(py)) } fn push_item<'py>( builder: &mut Self::Builder<'py>, key: Bound<'py, PyAny>, value: Bound<'py, PyAny>, ) -> PyResult<()> { builder.set_item(key, value) } fn finish<'py>(builder: Self::Builder<'py>) -> PyResult> { Ok(builder.into_mapping()) } } /// Adapter type to use an unnamed mapping type, i.e. one that implements /// [`PythonizeMappingType`], as a named mapping type, i.e. one that implements /// [`PythonizeNamedMappingType`]. The adapter simply drops the provided name. /// /// This adapter is commonly applied to use the same unnamed mapping type for /// both [`PythonizeTypes::Map`] and [`PythonizeTypes::NamedMap`] while only /// implementing [`PythonizeMappingType`]. pub struct PythonizeUnnamedMappingAdapter { _unnamed: T, } impl PythonizeNamedMappingType for PythonizeUnnamedMappingAdapter { type Builder<'py> = T::Builder<'py>; fn builder<'py>( py: Python<'py>, len: usize, _name: &'static str, ) -> PyResult> { T::builder(py, Some(len)) } fn push_field<'py>( builder: &mut Self::Builder<'py>, name: Bound<'py, PyString>, value: Bound<'py, PyAny>, ) -> PyResult<()> { T::push_item(builder, name.into_any(), value) } fn finish<'py>(builder: Self::Builder<'py>) -> PyResult> { T::finish(builder) } } impl PythonizeListType for PyList { fn create_sequence<'py, T, U>( py: Python<'py>, elements: impl IntoIterator, ) -> PyResult> where T: IntoPyObject<'py>, U: ExactSizeIterator, { Ok(PyList::new(py, elements)?.into_sequence()) } } impl PythonizeListType for PyTuple { fn create_sequence<'py, T, U>( py: Python<'py>, elements: impl IntoIterator, ) -> PyResult> where T: IntoPyObject<'py>, U: ExactSizeIterator, { Ok(PyTuple::new(py, elements)?.into_sequence()) } } pub struct PythonizeDefault; impl PythonizeTypes for PythonizeDefault { type Map = PyDict; type NamedMap = PythonizeUnnamedMappingAdapter; type List = PyList; } /// Attempt to convert the given data into a Python object pub fn pythonize<'py, T>(py: Python<'py>, value: &T) -> Result> where T: ?Sized + Serialize, { value.serialize(Pythonizer::new(py)) } /// Attempt to convert the given data into a Python object. /// Also uses custom mapping python class for serialization. pub fn pythonize_custom<'py, P, T>(py: Python<'py>, value: &T) -> Result> where T: ?Sized + Serialize, P: PythonizeTypes, { value.serialize(Pythonizer::custom::

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

, } impl<'py, P> From> for Pythonizer<'py, P> { fn from(py: Python<'py>) -> Self { Self { py, _types: PhantomData, } } } impl<'py> Pythonizer<'py, PythonizeDefault> { /// Creates a serializer to convert data into a Python object using the default mapping class pub fn new(py: Python<'py>) -> Self { Self::from(py) } /// Creates a serializer to convert data into a Python object using a custom mapping class pub fn custom

(py: Python<'py>) -> Pythonizer<'py, P> { Pythonizer::from(py) } } #[doc(hidden)] pub struct PythonCollectionSerializer<'py, P> { items: Vec>, py: Python<'py>, _types: PhantomData

, } #[doc(hidden)] pub struct PythonTupleVariantSerializer<'py, P> { name: &'static str, variant: &'static str, inner: PythonCollectionSerializer<'py, P>, } #[doc(hidden)] pub struct PythonStructVariantSerializer<'py, P: PythonizeTypes> { name: &'static str, variant: &'static str, inner: PythonStructDictSerializer<'py, P>, } #[doc(hidden)] pub struct PythonStructDictSerializer<'py, P: PythonizeTypes> { py: Python<'py>, builder: ::Builder<'py>, _types: PhantomData

, } #[doc(hidden)] pub struct PythonMapSerializer<'py, P: PythonizeTypes> { py: Python<'py>, builder: ::Builder<'py>, key: Option>, _types: PhantomData

, } impl<'py, P: PythonizeTypes> Pythonizer<'py, P> { /// The default implementation for serialisation functions. #[inline] fn serialise_default(self, v: T) -> Result> where T: IntoPyObject<'py>, >::Error: Into, { v.into_pyobject(self.py) .map(|x| x.into_any().into_bound()) .map_err(Into::into) } } impl<'py, P: PythonizeTypes> ser::Serializer for Pythonizer<'py, P> { type Ok = Bound<'py, PyAny>; type Error = PythonizeError; type SerializeSeq = PythonCollectionSerializer<'py, P>; type SerializeTuple = PythonCollectionSerializer<'py, P>; type SerializeTupleStruct = PythonCollectionSerializer<'py, P>; type SerializeTupleVariant = PythonTupleVariantSerializer<'py, P>; type SerializeMap = PythonMapSerializer<'py, P>; type SerializeStruct = PythonStructDictSerializer<'py, P>; type SerializeStructVariant = PythonStructVariantSerializer<'py, P>; fn serialize_bool(self, v: bool) -> Result> { self.serialise_default(v) } fn serialize_i8(self, v: i8) -> Result> { self.serialise_default(v) } fn serialize_i16(self, v: i16) -> Result> { self.serialise_default(v) } fn serialize_i32(self, v: i32) -> Result> { self.serialise_default(v) } fn serialize_i64(self, v: i64) -> Result> { self.serialise_default(v) } fn serialize_u8(self, v: u8) -> Result> { self.serialise_default(v) } fn serialize_u16(self, v: u16) -> Result> { self.serialise_default(v) } fn serialize_u32(self, v: u32) -> Result> { self.serialise_default(v) } fn serialize_u64(self, v: u64) -> Result> { self.serialise_default(v) } fn serialize_f32(self, v: f32) -> Result> { self.serialise_default(v) } fn serialize_f64(self, v: f64) -> Result> { self.serialise_default(v) } fn serialize_char(self, v: char) -> Result> { self.serialize_str(&v.to_string()) } fn serialize_str(self, v: &str) -> Result> { Ok(PyString::new(self.py, v).into_any()) } fn serialize_bytes(self, v: &[u8]) -> Result> { self.serialise_default(v) } fn serialize_none(self) -> Result> { Ok(self.py.None().into_bound(self.py)) } fn serialize_some(self, value: &T) -> Result> where T: ?Sized + Serialize, { value.serialize(self) } fn serialize_unit(self) -> Result> { self.serialize_none() } fn serialize_unit_struct(self, _name: &'static str) -> Result> { self.serialize_none() } fn serialize_unit_variant( self, _name: &'static str, _variant_index: u32, variant: &'static str, ) -> Result> { self.serialize_str(variant) } fn serialize_newtype_struct( self, _name: &'static str, value: &T, ) -> Result> where T: ?Sized + Serialize, { value.serialize(self) } fn serialize_newtype_variant( self, name: &'static str, _variant_index: u32, variant: &'static str, value: &T, ) -> Result> where T: ?Sized + Serialize, { let mut m = P::NamedMap::builder(self.py, 1, name)?; P::NamedMap::push_field( &mut m, PyString::new(self.py, variant), value.serialize(self)?, )?; Ok(P::NamedMap::finish(m)?.into_any()) } fn serialize_seq(self, len: Option) -> Result> { let items = match len { Some(len) => Vec::with_capacity(len), None => Vec::new(), }; Ok(PythonCollectionSerializer { items, py: self.py, _types: PhantomData, }) } fn serialize_tuple(self, len: usize) -> Result> { Ok(PythonCollectionSerializer { items: Vec::with_capacity(len), py: self.py, _types: PhantomData, }) } fn serialize_tuple_struct( self, _name: &'static str, len: usize, ) -> Result> { self.serialize_tuple(len) } fn serialize_tuple_variant( self, name: &'static str, _variant_index: u32, variant: &'static str, len: usize, ) -> Result> { let inner = self.serialize_tuple(len)?; Ok(PythonTupleVariantSerializer { name, variant, inner, }) } fn serialize_map(self, len: Option) -> Result> { Ok(PythonMapSerializer { builder: P::Map::builder(self.py, len)?, key: None, py: self.py, _types: PhantomData, }) } fn serialize_struct( self, name: &'static str, len: usize, ) -> Result> { Ok(PythonStructDictSerializer { py: self.py, builder: P::NamedMap::builder(self.py, len, name)?, _types: PhantomData, }) } fn serialize_struct_variant( self, name: &'static str, _variant_index: u32, variant: &'static str, len: usize, ) -> Result> { Ok(PythonStructVariantSerializer { name, variant, inner: PythonStructDictSerializer { py: self.py, builder: P::NamedMap::builder(self.py, len, variant)?, _types: PhantomData, }, }) } } impl<'py, P: PythonizeTypes> ser::SerializeSeq for PythonCollectionSerializer<'py, P> { type Ok = Bound<'py, PyAny>; type Error = PythonizeError; fn serialize_element(&mut self, value: &T) -> Result<()> where T: ?Sized + Serialize, { self.items.push(pythonize_custom::(self.py, value)?); Ok(()) } fn end(self) -> Result> { let instance = P::List::create_sequence(self.py, self.items)?; Ok(instance.into_pyobject(self.py)?.into_any()) } } impl<'py, P: PythonizeTypes> ser::SerializeTuple for PythonCollectionSerializer<'py, P> { type Ok = Bound<'py, PyAny>; type Error = PythonizeError; fn serialize_element(&mut self, value: &T) -> Result<()> where T: ?Sized + Serialize, { ser::SerializeSeq::serialize_element(self, value) } fn end(self) -> Result> { Ok(PyTuple::new(self.py, self.items)?.into_any()) } } impl<'py, P: PythonizeTypes> ser::SerializeTupleStruct for PythonCollectionSerializer<'py, P> { type Ok = Bound<'py, PyAny>; type Error = PythonizeError; fn serialize_field(&mut self, value: &T) -> Result<()> where T: ?Sized + Serialize, { ser::SerializeSeq::serialize_element(self, value) } fn end(self) -> Result> { ser::SerializeTuple::end(self) } } impl<'py, P: PythonizeTypes> ser::SerializeTupleVariant for PythonTupleVariantSerializer<'py, P> { type Ok = Bound<'py, PyAny>; type Error = PythonizeError; fn serialize_field(&mut self, value: &T) -> Result<()> where T: ?Sized + Serialize, { ser::SerializeSeq::serialize_element(&mut self.inner, value) } fn end(self) -> Result> { let mut m = P::NamedMap::builder(self.inner.py, 1, self.name)?; P::NamedMap::push_field( &mut m, PyString::new(self.inner.py, self.variant), ser::SerializeTuple::end(self.inner)?, )?; Ok(P::NamedMap::finish(m)?.into_any()) } } impl<'py, P: PythonizeTypes> ser::SerializeMap for PythonMapSerializer<'py, P> { type Ok = Bound<'py, PyAny>; type Error = PythonizeError; fn serialize_key(&mut self, key: &T) -> Result<()> where T: ?Sized + Serialize, { self.key = Some(pythonize_custom::(self.py, key)?); Ok(()) } fn serialize_value(&mut self, value: &T) -> Result<()> where T: ?Sized + Serialize, { P::Map::push_item( &mut self.builder, self.key .take() .expect("serialize_value should always be called after serialize_key"), pythonize_custom::(self.py, value)?, )?; Ok(()) } fn end(self) -> Result> { Ok(P::Map::finish(self.builder)?.into_any()) } } impl<'py, P: PythonizeTypes> ser::SerializeStruct for PythonStructDictSerializer<'py, P> { type Ok = Bound<'py, PyAny>; type Error = PythonizeError; fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<()> where T: ?Sized + Serialize, { P::NamedMap::push_field( &mut self.builder, PyString::new(self.py, key), pythonize_custom::(self.py, value)?, )?; Ok(()) } fn end(self) -> Result> { Ok(P::NamedMap::finish(self.builder)?.into_any()) } } impl<'py, P: PythonizeTypes> ser::SerializeStructVariant for PythonStructVariantSerializer<'py, P> { type Ok = Bound<'py, PyAny>; type Error = PythonizeError; fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<()> where T: ?Sized + Serialize, { P::NamedMap::push_field( &mut self.inner.builder, PyString::new(self.inner.py, key), pythonize_custom::(self.inner.py, value)?, )?; Ok(()) } fn end(self) -> Result> { let v = P::NamedMap::finish(self.inner.builder)?; let mut m = P::NamedMap::builder(self.inner.py, 1, self.name)?; P::NamedMap::push_field( &mut m, PyString::new(self.inner.py, self.variant), v.into_any(), )?; Ok(P::NamedMap::finish(m)?.into_any()) } } #[cfg(test)] mod test { use super::pythonize; use maplit::hashmap; use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::pybacked::PyBackedStr; use pyo3::types::{PyBytes, PyDict}; use serde::Serialize; fn test_ser(src: T, expected: &str) where T: Serialize, { Python::attach(|py| -> PyResult<()> { let obj = pythonize(py, &src)?; let locals = PyDict::new(py); locals.set_item("obj", obj)?; py.run( c_str!("import json; result = json.dumps(obj, separators=(',', ':'))"), None, Some(&locals), )?; let result = locals.get_item("result")?.unwrap(); let result = result.extract::()?; assert_eq!(result, expected); assert_eq!(serde_json::to_string(&src).unwrap(), expected); Ok(()) }) .unwrap(); } #[test] fn test_empty_struct() { #[derive(Serialize)] struct Empty; test_ser(Empty, "null"); } #[test] fn test_struct() { #[derive(Serialize)] struct Struct { foo: String, bar: usize, } test_ser( Struct { foo: "foo".to_string(), bar: 5, }, r#"{"foo":"foo","bar":5}"#, ); } #[test] fn test_nested_struct() { #[derive(Serialize)] struct Foo { name: String, bar: Bar, } #[derive(Serialize)] struct Bar { name: String, } test_ser( Foo { name: "foo".to_string(), bar: Bar { name: "bar".to_string(), }, }, r#"{"name":"foo","bar":{"name":"bar"}}"#, ) } #[test] fn test_tuple_struct() { #[derive(Serialize)] struct TupleStruct(String, usize); test_ser(TupleStruct("foo".to_string(), 5), r#"["foo",5]"#); } #[test] fn test_tuple() { test_ser(("foo", 5), r#"["foo",5]"#); } #[test] fn test_vec() { test_ser(vec![1, 2, 3], r#"[1,2,3]"#); } #[test] fn test_map() { test_ser(hashmap! {"foo" => "foo"}, r#"{"foo":"foo"}"#); } #[test] fn test_enum_unit_variant() { #[derive(Serialize)] enum E { Empty, } test_ser(E::Empty, r#""Empty""#); } #[test] fn test_enum_tuple_variant() { #[derive(Serialize)] enum E { Tuple(i32, String), } test_ser(E::Tuple(5, "foo".to_string()), r#"{"Tuple":[5,"foo"]}"#); } #[test] fn test_enum_newtype_variant() { #[derive(Serialize)] enum E { NewType(String), } test_ser(E::NewType("foo".to_string()), r#"{"NewType":"foo"}"#); } #[test] fn test_enum_struct_variant() { #[derive(Serialize)] enum E { Struct { foo: String, bar: usize }, } test_ser( E::Struct { foo: "foo".to_string(), bar: 5, }, r#"{"Struct":{"foo":"foo","bar":5}}"#, ); } #[test] fn test_integers() { #[derive(Serialize)] struct Integers { a: i8, b: i16, c: i32, d: i64, e: u8, f: u16, g: u32, h: u64, } test_ser( Integers { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, }, r#"{"a":1,"b":2,"c":3,"d":4,"e":5,"f":6,"g":7,"h":8}"#, ) } #[test] fn test_floats() { #[derive(Serialize)] struct Floats { a: f32, b: f64, } test_ser(Floats { a: 1.0, b: 2.0 }, r#"{"a":1.0,"b":2.0}"#) } #[test] fn test_char() { #[derive(Serialize)] struct Char { a: char, } test_ser(Char { a: 'a' }, r#"{"a":"a"}"#) } #[test] fn test_bool() { test_ser(true, "true"); test_ser(false, "false"); } #[test] fn test_none() { #[derive(Serialize)] struct S; test_ser((), "null"); test_ser(S, "null"); test_ser(Some(1), "1"); test_ser(None::, "null"); } #[test] fn test_bytes() { // serde treats &[u8] as a sequence of integers due to lack of specialization test_ser(b"foo", "[102,111,111]"); Python::attach(|py| { assert!(pythonize(py, serde_bytes::Bytes::new(b"foo")) .expect("bytes will always serialize successfully") .eq(&PyBytes::new(py, b"foo")) .expect("bytes will always compare successfully")); }); } } pythonize-0.27.0/tests/test_custom_types.rs000064400000000000000000000170711046102023000172560ustar 00000000000000use std::collections::HashMap; use pyo3::{ exceptions::{PyIndexError, PyKeyError}, prelude::*, types::{PyDict, PyMapping, PySequence, PyTuple}, IntoPyObjectExt, }; use pythonize::{ depythonize, pythonize_custom, PythonizeListType, PythonizeMappingType, PythonizeNamedMappingType, PythonizeTypes, PythonizeUnnamedMappingAdapter, Pythonizer, }; use serde::Serialize; use serde_json::{json, Value}; #[pyclass(sequence)] struct CustomList { items: Vec>, } #[pymethods] impl CustomList { fn __len__(&self) -> usize { self.items.len() } fn __getitem__(&self, idx: isize) -> PyResult> { self.items .get(idx as usize) .cloned() .ok_or_else(|| PyIndexError::new_err(idx)) } } impl PythonizeListType for CustomList { fn create_sequence<'py, T, U>( py: Python<'py>, elements: impl IntoIterator, ) -> PyResult> where T: IntoPyObject<'py>, U: ExactSizeIterator, { let sequence = Bound::new( py, CustomList { items: elements .into_iter() .map(|item| item.into_py_any(py)) .collect::>()?, }, )?; Ok(unsafe { sequence.cast_into_unchecked() }) } } struct PythonizeCustomList; impl<'py> PythonizeTypes for PythonizeCustomList { type Map = PyDict; type NamedMap = PythonizeUnnamedMappingAdapter; type List = CustomList; } #[test] fn test_custom_list() { Python::attach(|py| { PySequence::register::(py).unwrap(); let serialized = pythonize_custom::(py, &json!([1, 2, 3])).unwrap(); assert!(serialized.is_instance_of::()); let deserialized: Value = depythonize(&serialized).unwrap(); assert_eq!(deserialized, json!([1, 2, 3])); }) } #[pyclass(mapping)] struct CustomDict { items: HashMap>, } #[pymethods] impl CustomDict { fn __len__(&self) -> usize { self.items.len() } fn __getitem__(&self, key: String) -> PyResult> { self.items .get(&key) .cloned() .ok_or_else(|| PyKeyError::new_err(key)) } fn __setitem__(&mut self, key: String, value: Py) { self.items.insert(key, value); } fn keys(&self) -> Vec<&String> { self.items.keys().collect() } fn values(&self) -> Vec> { self.items.values().cloned().collect() } } impl PythonizeMappingType for CustomDict { type Builder<'py> = Bound<'py, CustomDict>; fn builder<'py>(py: Python<'py>, len: Option) -> PyResult> { Bound::new( py, CustomDict { items: HashMap::with_capacity(len.unwrap_or(0)), }, ) } fn push_item<'py>( builder: &mut Self::Builder<'py>, key: Bound<'py, PyAny>, value: Bound<'py, PyAny>, ) -> PyResult<()> { unsafe { builder.cast_unchecked::() }.set_item(key, value) } fn finish<'py>(builder: Self::Builder<'py>) -> PyResult> { Ok(unsafe { builder.cast_into_unchecked() }) } } struct PythonizeCustomDict; impl<'py> PythonizeTypes for PythonizeCustomDict { type Map = CustomDict; type NamedMap = PythonizeUnnamedMappingAdapter; type List = PyTuple; } #[test] fn test_custom_dict() { Python::attach(|py| { PyMapping::register::(py).unwrap(); let serialized = pythonize_custom::(py, &json!({ "hello": 1, "world": 2 })) .unwrap(); assert!(serialized.is_instance_of::()); let deserialized: Value = depythonize(&serialized).unwrap(); assert_eq!(deserialized, json!({ "hello": 1, "world": 2 })); }) } #[test] fn test_tuple() { Python::attach(|py| { PyMapping::register::(py).unwrap(); let serialized = pythonize_custom::(py, &json!([1, 2, 3, 4])).unwrap(); assert!(serialized.is_instance_of::()); let deserialized: Value = depythonize(&serialized).unwrap(); assert_eq!(deserialized, json!([1, 2, 3, 4])); }) } #[test] fn test_pythonizer_can_be_created() { // https://github.com/davidhewitt/pythonize/pull/56 Python::attach(|py| { let sample = json!({ "hello": 1, "world": 2 }); assert!(sample .serialize(Pythonizer::new(py)) .unwrap() .is_instance_of::()); assert!(sample .serialize(Pythonizer::custom::(py)) .unwrap() .is_instance_of::()); }) } #[pyclass(mapping)] struct NamedCustomDict { name: String, items: HashMap>, } #[pymethods] impl NamedCustomDict { fn __len__(&self) -> usize { self.items.len() } fn __getitem__(&self, key: String) -> PyResult> { self.items .get(&key) .cloned() .ok_or_else(|| PyKeyError::new_err(key)) } fn __setitem__(&mut self, key: String, value: Py) { self.items.insert(key, value); } fn keys(&self) -> Vec<&String> { self.items.keys().collect() } fn values(&self) -> Vec> { self.items.values().cloned().collect() } } impl PythonizeNamedMappingType for NamedCustomDict { type Builder<'py> = Bound<'py, NamedCustomDict>; fn builder<'py>( py: Python<'py>, len: usize, name: &'static str, ) -> PyResult> { Bound::new( py, NamedCustomDict { name: String::from(name), items: HashMap::with_capacity(len), }, ) } fn push_field<'py>( builder: &mut Self::Builder<'py>, name: Bound<'py, pyo3::types::PyString>, value: Bound<'py, PyAny>, ) -> PyResult<()> { unsafe { builder.cast_unchecked::() }.set_item(name, value) } fn finish<'py>(builder: Self::Builder<'py>) -> PyResult> { Ok(unsafe { builder.cast_into_unchecked() }) } } struct PythonizeNamedCustomDict; impl<'py> PythonizeTypes for PythonizeNamedCustomDict { type Map = CustomDict; type NamedMap = NamedCustomDict; type List = PyTuple; } #[derive(Serialize)] struct Struct { hello: u8, world: i8, } #[test] fn test_custom_unnamed_dict() { Python::attach(|py| { PyMapping::register::(py).unwrap(); let serialized = pythonize_custom::(py, &Struct { hello: 1, world: 2 }).unwrap(); assert!(serialized.is_instance_of::()); let deserialized: Value = depythonize(&serialized).unwrap(); assert_eq!(deserialized, json!({ "hello": 1, "world": 2 })); }) } #[test] fn test_custom_named_dict() { Python::attach(|py| { PyMapping::register::(py).unwrap(); let serialized = pythonize_custom::(py, &Struct { hello: 1, world: 2 }) .unwrap(); let named: Bound = serialized.extract().unwrap(); assert_eq!(named.borrow().name, "Struct"); let deserialized: Value = depythonize(&serialized).unwrap(); assert_eq!(deserialized, json!({ "hello": 1, "world": 2 })); }) } pythonize-0.27.0/tests/test_with_serde_path_to_err.rs000064400000000000000000000141211046102023000212340ustar 00000000000000use std::collections::BTreeMap; use pyo3::{ prelude::*, types::{PyDict, PyList}, }; use pythonize::{PythonizeTypes, PythonizeUnnamedMappingAdapter}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] struct Root { root_key: String, root_map: BTreeMap>, } impl<'py, T> PythonizeTypes for Root { type Map = PyDict; type NamedMap = PythonizeUnnamedMappingAdapter; type List = PyList; } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] struct Nested { nested_key: T, } #[derive(Deserialize, Debug, PartialEq, Eq)] struct CannotSerialize {} impl Serialize for CannotSerialize { fn serialize(&self, _serializer: S) -> Result where S: serde::Serializer, { Err(serde::ser::Error::custom( "something went intentionally wrong", )) } } #[test] fn test_de_valid() { Python::attach(|py| { let pyroot = PyDict::new(py); pyroot.set_item("root_key", "root_value").unwrap(); let nested = PyDict::new(py); let nested_0 = PyDict::new(py); nested_0.set_item("nested_key", "nested_value_0").unwrap(); nested.set_item("nested_0", nested_0).unwrap(); let nested_1 = PyDict::new(py); nested_1.set_item("nested_key", "nested_value_1").unwrap(); nested.set_item("nested_1", nested_1).unwrap(); pyroot.set_item("root_map", nested).unwrap(); let de = &mut pythonize::Depythonizer::from_object(&pyroot); let root: Root = serde_path_to_error::deserialize(de).unwrap(); assert_eq!( root, Root { root_key: String::from("root_value"), root_map: BTreeMap::from([ ( String::from("nested_0"), Nested { nested_key: String::from("nested_value_0") } ), ( String::from("nested_1"), Nested { nested_key: String::from("nested_value_1") } ) ]) } ); }) } #[test] fn test_de_invalid() { Python::attach(|py| { let pyroot = PyDict::new(py); pyroot.set_item("root_key", "root_value").unwrap(); let nested = PyDict::new(py); let nested_0 = PyDict::new(py); nested_0.set_item("nested_key", "nested_value_0").unwrap(); nested.set_item("nested_0", nested_0).unwrap(); let nested_1 = PyDict::new(py); nested_1.set_item("nested_key", 1).unwrap(); nested.set_item("nested_1", nested_1).unwrap(); pyroot.set_item("root_map", nested).unwrap(); let de = &mut pythonize::Depythonizer::from_object(&pyroot); let err = serde_path_to_error::deserialize::<_, Root>(de).unwrap_err(); assert_eq!(err.path().to_string(), "root_map.nested_1.nested_key"); assert_eq!( err.to_string(), "root_map.nested_1.nested_key: unexpected type: 'int' object cannot be cast as 'str'" ); }) } #[test] fn test_ser_valid() { Python::attach(|py| { let root = Root { root_key: String::from("root_value"), root_map: BTreeMap::from([ ( String::from("nested_0"), Nested { nested_key: String::from("nested_value_0"), }, ), ( String::from("nested_1"), Nested { nested_key: String::from("nested_value_1"), }, ), ]), }; let ser = pythonize::Pythonizer::>::from(py); let pyroot: Bound<'_, PyAny> = serde_path_to_error::serialize(&root, ser).unwrap(); let pyroot = pyroot.cast::().unwrap(); assert_eq!(pyroot.len(), 2); let root_value: String = pyroot .get_item("root_key") .unwrap() .unwrap() .extract() .unwrap(); assert_eq!(root_value, "root_value"); let root_map = pyroot .get_item("root_map") .unwrap() .unwrap() .cast_into::() .unwrap(); assert_eq!(root_map.len(), 2); let nested_0 = root_map .get_item("nested_0") .unwrap() .unwrap() .cast_into::() .unwrap(); assert_eq!(nested_0.len(), 1); let nested_key_0: String = nested_0 .get_item("nested_key") .unwrap() .unwrap() .extract() .unwrap(); assert_eq!(nested_key_0, "nested_value_0"); let nested_1 = root_map .get_item("nested_1") .unwrap() .unwrap() .cast_into::() .unwrap(); assert_eq!(nested_1.len(), 1); let nested_key_1: String = nested_1 .get_item("nested_key") .unwrap() .unwrap() .extract() .unwrap(); assert_eq!(nested_key_1, "nested_value_1"); }); } #[test] fn test_ser_invalid() { Python::attach(|py| { let root = Root { root_key: String::from("root_value"), root_map: BTreeMap::from([ ( String::from("nested_0"), Nested { nested_key: CannotSerialize {}, }, ), ( String::from("nested_1"), Nested { nested_key: CannotSerialize {}, }, ), ]), }; let ser = pythonize::Pythonizer::>::from(py); let err = serde_path_to_error::serialize(&root, ser).unwrap_err(); assert_eq!(err.path().to_string(), "root_map.nested_0.nested_key"); }); }