From 49ee947d5f341607f63504a9a9549f67edea81e0 Mon Sep 17 00:00:00 2001 From: Dylan DPC <99973273+Dylan-DPC@users.noreply.github.com> Date: Fri, 25 Apr 2025 16:30:41 +0530 Subject: [PATCH 01/10] Update Cargo.toml (#87) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 57de5e6..8903051 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ pyo3 = { version = "0.24", default-features = false } [dev-dependencies] serde = { version = "1.0", default-features = false, features = ["derive"] } -pyo3 = { version = "0.24", default-features = false, features = ["auto-initialize", "macros", "py-clone"] } +pyo3 = { version = "0.24.1", default-features = false, features = ["auto-initialize", "macros", "py-clone"] } serde_json = "1.0" serde_bytes = "0.11" maplit = "1.0.2" From 6f51e936d926e62b229f0ee850a699acaf66ce65 Mon Sep 17 00:00:00 2001 From: jesse Date: Fri, 23 May 2025 04:34:01 -0700 Subject: [PATCH 02/10] update pyo3 version to 0.25.x (#88) --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8903051..b9d4df2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pythonize" -version = "0.24.0" +version = "0.25.0" authors = ["David Hewitt <1939362+davidhewitt@users.noreply.github.com>"] edition = "2021" rust-version = "1.63" @@ -13,11 +13,11 @@ documentation = "https://docs.rs/crate/pythonize/" [dependencies] serde = { version = "1.0", default-features = false, features = ["std"] } -pyo3 = { version = "0.24", default-features = false } +pyo3 = { version = "0.25", default-features = false } [dev-dependencies] serde = { version = "1.0", default-features = false, features = ["derive"] } -pyo3 = { version = "0.24.1", default-features = false, features = ["auto-initialize", "macros", "py-clone"] } +pyo3 = { version = "0.25", default-features = false, features = ["auto-initialize", "macros", "py-clone"] } serde_json = "1.0" serde_bytes = "0.11" maplit = "1.0.2" From 096f83e7b7ec436524c6561d3a99be5159e9bee1 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 23 May 2025 12:35:38 +0100 Subject: [PATCH 03/10] add ci to release crate (#89) --- .github/release.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/release.yml diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..5f21b46 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,21 @@ +name: Release Rust Crate + +on: + push: + tags: + - "v*" + +jobs: + release: + runs-on: ubuntu-latest + environment: release + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - name: Publish to crates.io + run: cargo publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} From 435870cd014456dc13a384434f6f43838152c459 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 23 May 2025 12:38:38 +0100 Subject: [PATCH 04/10] fixup workflow location (#90) --- .github/{ => workflows}/release.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{ => workflows}/release.yml (100%) diff --git a/.github/release.yml b/.github/workflows/release.yml similarity index 100% rename from .github/release.yml rename to .github/workflows/release.yml From ddfe9f111e8b2f07ec0e5ee5f211074b2880cd73 Mon Sep 17 00:00:00 2001 From: jesse Date: Sat, 14 Jun 2025 03:02:14 -0700 Subject: [PATCH 05/10] fix-typos (#92) --- src/de.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/de.rs b/src/de.rs index 49c0889..2860e48 100644 --- a/src/de.rs +++ b/src/de.rs @@ -121,7 +121,7 @@ impl<'de> de::Deserializer<'de> for &'_ mut Depythonizer<'_, '_> { self.deserialize_str(visitor) } // Continue with cases which are slower to check because they go - // throuh `isinstance` machinery + // through `isinstance` machinery else if obj.is_instance_of::() || obj.is_instance_of::() { self.deserialize_bytes(visitor) } else if obj.is_instance_of::() { @@ -826,7 +826,7 @@ mod test { #[test] fn test_int_limits() { Python::with_gil(|py| { - // serde_json::Value supports u64 and i64 as maxiumum sizes + // 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(); From e64436cd21504e3ebd6bb25da0ea36ca42c1e8a3 Mon Sep 17 00:00:00 2001 From: jesse Date: Sat, 30 Aug 2025 01:53:40 -0700 Subject: [PATCH 06/10] did I `GAT` this right? (#91) --- Cargo.toml | 2 +- src/ser.rs | 114 +++++++++++++-------------- tests/test_custom_types.rs | 38 +++++---- tests/test_with_serde_path_to_err.rs | 4 +- 4 files changed, 80 insertions(+), 78 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b9d4df2..d636384 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "pythonize" version = "0.25.0" authors = ["David Hewitt <1939362+davidhewitt@users.noreply.github.com>"] edition = "2021" -rust-version = "1.63" +rust-version = "1.65" license = "MIT" description = "Serde Serializer & Deserializer from Rust <--> Python, backed by PyO3." homepage = "https://github.com/davidhewitt/pythonize" diff --git a/src/ser.rs b/src/ser.rs index 513a8e2..dce22e3 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -9,44 +9,46 @@ use serde::{ser, Serialize}; use crate::error::{PythonizeError, Result}; -// TODO: move 'py lifetime into builder once GATs are available in MSRV /// Trait for types which can represent a Python mapping -pub trait PythonizeMappingType<'py> { +pub trait PythonizeMappingType { /// Builder type for Python mappings - type Builder; + type Builder<'py>: 'py; /// Create a builder for a Python mapping - fn builder(py: Python<'py>, len: Option) -> PyResult; + fn builder<'py>(py: Python<'py>, len: Option) -> PyResult>; /// Adds the key-value item to the mapping being built - fn push_item( - builder: &mut Self::Builder, + fn push_item<'py>( + builder: &mut Self::Builder<'py>, key: Bound<'py, PyAny>, value: Bound<'py, PyAny>, ) -> PyResult<()>; /// Build the Python mapping - fn finish(builder: Self::Builder) -> PyResult>; + fn finish<'py>(builder: Self::Builder<'py>) -> PyResult>; } -// TODO: move 'py lifetime into builder once GATs are available in MSRV /// Trait for types which can represent a Python mapping and have a name -pub trait PythonizeNamedMappingType<'py> { +pub trait PythonizeNamedMappingType { /// Builder type for Python mappings with a name - type Builder; + type Builder<'py>: 'py; /// Create a builder for a Python mapping with a name - fn builder(py: Python<'py>, len: usize, name: &'static str) -> PyResult; + fn builder<'py>( + py: Python<'py>, + len: usize, + name: &'static str, + ) -> PyResult>; /// Adds the field to the named mapping being built - fn push_field( - builder: &mut Self::Builder, + fn push_field<'py>( + builder: &mut Self::Builder<'py>, name: Bound<'py, PyString>, value: Bound<'py, PyAny>, ) -> PyResult<()>; /// Build the Python mapping - fn finish(builder: Self::Builder) -> PyResult>; + fn finish<'py>(builder: Self::Builder<'py>) -> PyResult>; } /// Trait for types which can represent a Python sequence @@ -61,33 +63,32 @@ pub trait PythonizeListType: Sized { U: ExactSizeIterator; } -// TODO: remove 'py lifetime once GATs are available in MSRV /// Custom types for serialization -pub trait PythonizeTypes<'py> { +pub trait PythonizeTypes { /// Python map type (should be representable as python mapping) - type Map: PythonizeMappingType<'py>; + type Map: PythonizeMappingType; /// Python (struct-like) named map type (should be representable as python mapping) - type NamedMap: PythonizeNamedMappingType<'py>; + type NamedMap: PythonizeNamedMappingType; /// Python sequence type (should be representable as python sequence) type List: PythonizeListType; } -impl<'py> PythonizeMappingType<'py> for PyDict { - type Builder = Bound<'py, Self>; +impl PythonizeMappingType for PyDict { + type Builder<'py> = Bound<'py, Self>; - fn builder(py: Python<'py>, _len: Option) -> PyResult { + fn builder<'py>(py: Python<'py>, _len: Option) -> PyResult> { Ok(Self::new(py)) } - fn push_item( - builder: &mut Self::Builder, + 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(builder: Self::Builder) -> PyResult> { + fn finish<'py>(builder: Self::Builder<'py>) -> PyResult> { Ok(builder.into_mapping()) } } @@ -99,30 +100,31 @@ impl<'py> PythonizeMappingType<'py> for PyDict { /// 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<'py, T: PythonizeMappingType<'py>> { +pub struct PythonizeUnnamedMappingAdapter { _unnamed: T, - _marker: PhantomData<&'py ()>, } -impl<'py, T: PythonizeMappingType<'py>> PythonizeNamedMappingType<'py> - for PythonizeUnnamedMappingAdapter<'py, T> -{ - type Builder = >::Builder; +impl PythonizeNamedMappingType for PythonizeUnnamedMappingAdapter { + type Builder<'py> = T::Builder<'py>; - fn builder(py: Python<'py>, len: usize, _name: &'static str) -> PyResult { - ::builder(py, Some(len)) + fn builder<'py>( + py: Python<'py>, + len: usize, + _name: &'static str, + ) -> PyResult> { + T::builder(py, Some(len)) } - fn push_field( - builder: &mut Self::Builder, + fn push_field<'py>( + builder: &mut Self::Builder<'py>, name: Bound<'py, PyString>, value: Bound<'py, PyAny>, ) -> PyResult<()> { - ::push_item(builder, name.into_any(), value) + T::push_item(builder, name.into_any(), value) } - fn finish(builder: Self::Builder) -> PyResult> { - ::finish(builder) + fn finish<'py>(builder: Self::Builder<'py>) -> PyResult> { + T::finish(builder) } } @@ -154,9 +156,9 @@ impl PythonizeListType for PyTuple { pub struct PythonizeDefault; -impl<'py> PythonizeTypes<'py> for PythonizeDefault { +impl PythonizeTypes for PythonizeDefault { type Map = PyDict; - type NamedMap = PythonizeUnnamedMappingAdapter<'py, PyDict>; + type NamedMap = PythonizeUnnamedMappingAdapter; type List = PyList; } @@ -173,7 +175,7 @@ where pub fn pythonize_custom<'py, P, T>(py: Python<'py>, value: &T) -> Result> where T: ?Sized + Serialize, - P: PythonizeTypes<'py>, + P: PythonizeTypes, { value.serialize(Pythonizer::custom::

(py)) } @@ -221,28 +223,28 @@ pub struct PythonTupleVariantSerializer<'py, P> { } #[doc(hidden)] -pub struct PythonStructVariantSerializer<'py, P: PythonizeTypes<'py>> { +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>> { +pub struct PythonStructDictSerializer<'py, P: PythonizeTypes> { py: Python<'py>, - builder: >::Builder, + builder: ::Builder<'py>, _types: PhantomData

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

, } -impl<'py, P: PythonizeTypes<'py>> Pythonizer<'py, P> { +impl<'py, P: PythonizeTypes> Pythonizer<'py, P> { /// The default implementation for serialisation functions. #[inline] fn serialise_default(self, v: T) -> Result> @@ -256,7 +258,7 @@ impl<'py, P: PythonizeTypes<'py>> Pythonizer<'py, P> { } } -impl<'py, P: PythonizeTypes<'py>> ser::Serializer for Pythonizer<'py, P> { +impl<'py, P: PythonizeTypes> ser::Serializer for Pythonizer<'py, P> { type Ok = Bound<'py, PyAny>; type Error = PythonizeError; type SerializeSeq = PythonCollectionSerializer<'py, P>; @@ -464,7 +466,7 @@ impl<'py, P: PythonizeTypes<'py>> ser::Serializer for Pythonizer<'py, P> { } } -impl<'py, P: PythonizeTypes<'py>> ser::SerializeSeq for PythonCollectionSerializer<'py, P> { +impl<'py, P: PythonizeTypes> ser::SerializeSeq for PythonCollectionSerializer<'py, P> { type Ok = Bound<'py, PyAny>; type Error = PythonizeError; @@ -482,7 +484,7 @@ impl<'py, P: PythonizeTypes<'py>> ser::SerializeSeq for PythonCollectionSerializ } } -impl<'py, P: PythonizeTypes<'py>> ser::SerializeTuple for PythonCollectionSerializer<'py, P> { +impl<'py, P: PythonizeTypes> ser::SerializeTuple for PythonCollectionSerializer<'py, P> { type Ok = Bound<'py, PyAny>; type Error = PythonizeError; @@ -498,7 +500,7 @@ impl<'py, P: PythonizeTypes<'py>> ser::SerializeTuple for PythonCollectionSerial } } -impl<'py, P: PythonizeTypes<'py>> ser::SerializeTupleStruct for PythonCollectionSerializer<'py, P> { +impl<'py, P: PythonizeTypes> ser::SerializeTupleStruct for PythonCollectionSerializer<'py, P> { type Ok = Bound<'py, PyAny>; type Error = PythonizeError; @@ -514,9 +516,7 @@ impl<'py, P: PythonizeTypes<'py>> ser::SerializeTupleStruct for PythonCollection } } -impl<'py, P: PythonizeTypes<'py>> ser::SerializeTupleVariant - for PythonTupleVariantSerializer<'py, P> -{ +impl<'py, P: PythonizeTypes> ser::SerializeTupleVariant for PythonTupleVariantSerializer<'py, P> { type Ok = Bound<'py, PyAny>; type Error = PythonizeError; @@ -538,7 +538,7 @@ impl<'py, P: PythonizeTypes<'py>> ser::SerializeTupleVariant } } -impl<'py, P: PythonizeTypes<'py>> ser::SerializeMap for PythonMapSerializer<'py, P> { +impl<'py, P: PythonizeTypes> ser::SerializeMap for PythonMapSerializer<'py, P> { type Ok = Bound<'py, PyAny>; type Error = PythonizeError; @@ -569,7 +569,7 @@ impl<'py, P: PythonizeTypes<'py>> ser::SerializeMap for PythonMapSerializer<'py, } } -impl<'py, P: PythonizeTypes<'py>> ser::SerializeStruct for PythonStructDictSerializer<'py, P> { +impl<'py, P: PythonizeTypes> ser::SerializeStruct for PythonStructDictSerializer<'py, P> { type Ok = Bound<'py, PyAny>; type Error = PythonizeError; @@ -590,9 +590,7 @@ impl<'py, P: PythonizeTypes<'py>> ser::SerializeStruct for PythonStructDictSeria } } -impl<'py, P: PythonizeTypes<'py>> ser::SerializeStructVariant - for PythonStructVariantSerializer<'py, P> -{ +impl<'py, P: PythonizeTypes> ser::SerializeStructVariant for PythonStructVariantSerializer<'py, P> { type Ok = Bound<'py, PyAny>; type Error = PythonizeError; diff --git a/tests/test_custom_types.rs b/tests/test_custom_types.rs index d311c14..32c5768 100644 --- a/tests/test_custom_types.rs +++ b/tests/test_custom_types.rs @@ -58,9 +58,9 @@ impl PythonizeListType for CustomList { } struct PythonizeCustomList; -impl<'py> PythonizeTypes<'py> for PythonizeCustomList { +impl<'py> PythonizeTypes for PythonizeCustomList { type Map = PyDict; - type NamedMap = PythonizeUnnamedMappingAdapter<'py, PyDict>; + type NamedMap = PythonizeUnnamedMappingAdapter; type List = CustomList; } @@ -107,10 +107,10 @@ impl CustomDict { } } -impl<'py> PythonizeMappingType<'py> for CustomDict { - type Builder = Bound<'py, CustomDict>; +impl PythonizeMappingType for CustomDict { + type Builder<'py> = Bound<'py, CustomDict>; - fn builder(py: Python<'py>, len: Option) -> PyResult { + fn builder<'py>(py: Python<'py>, len: Option) -> PyResult> { Bound::new( py, CustomDict { @@ -119,23 +119,23 @@ impl<'py> PythonizeMappingType<'py> for CustomDict { ) } - fn push_item( - builder: &mut Self::Builder, + fn push_item<'py>( + builder: &mut Self::Builder<'py>, key: Bound<'py, PyAny>, value: Bound<'py, PyAny>, ) -> PyResult<()> { unsafe { builder.downcast_unchecked::() }.set_item(key, value) } - fn finish(builder: Self::Builder) -> PyResult> { + fn finish<'py>(builder: Self::Builder<'py>) -> PyResult> { Ok(unsafe { builder.into_any().downcast_into_unchecked() }) } } struct PythonizeCustomDict; -impl<'py> PythonizeTypes<'py> for PythonizeCustomDict { +impl<'py> PythonizeTypes for PythonizeCustomDict { type Map = CustomDict; - type NamedMap = PythonizeUnnamedMappingAdapter<'py, CustomDict>; + type NamedMap = PythonizeUnnamedMappingAdapter; type List = PyTuple; } @@ -215,10 +215,14 @@ impl NamedCustomDict { } } -impl<'py> PythonizeNamedMappingType<'py> for NamedCustomDict { - type Builder = Bound<'py, NamedCustomDict>; +impl PythonizeNamedMappingType for NamedCustomDict { + type Builder<'py> = Bound<'py, NamedCustomDict>; - fn builder(py: Python<'py>, len: usize, name: &'static str) -> PyResult { + fn builder<'py>( + py: Python<'py>, + len: usize, + name: &'static str, + ) -> PyResult> { Bound::new( py, NamedCustomDict { @@ -228,21 +232,21 @@ impl<'py> PythonizeNamedMappingType<'py> for NamedCustomDict { ) } - fn push_field( - builder: &mut Self::Builder, + fn push_field<'py>( + builder: &mut Self::Builder<'py>, name: Bound<'py, pyo3::types::PyString>, value: Bound<'py, PyAny>, ) -> PyResult<()> { unsafe { builder.downcast_unchecked::() }.set_item(name, value) } - fn finish(builder: Self::Builder) -> PyResult> { + fn finish<'py>(builder: Self::Builder<'py>) -> PyResult> { Ok(unsafe { builder.into_any().downcast_into_unchecked() }) } } struct PythonizeNamedCustomDict; -impl<'py> PythonizeTypes<'py> for PythonizeNamedCustomDict { +impl<'py> PythonizeTypes for PythonizeNamedCustomDict { type Map = CustomDict; type NamedMap = NamedCustomDict; type List = PyTuple; diff --git a/tests/test_with_serde_path_to_err.rs b/tests/test_with_serde_path_to_err.rs index 5f2b970..9c3b688 100644 --- a/tests/test_with_serde_path_to_err.rs +++ b/tests/test_with_serde_path_to_err.rs @@ -13,9 +13,9 @@ struct Root { root_map: BTreeMap>, } -impl<'py, T> PythonizeTypes<'py> for Root { +impl<'py, T> PythonizeTypes for Root { type Map = PyDict; - type NamedMap = PythonizeUnnamedMappingAdapter<'py, PyDict>; + type NamedMap = PythonizeUnnamedMappingAdapter; type List = PyList; } From bc3caf522f8d4ddfceda8c6ca438b5e6ff16972d Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 30 Aug 2025 10:19:23 +0100 Subject: [PATCH 07/10] release: 0.26 (#95) * release: 0.26 * use trusted publishing for release --- .github/workflows/release.yml | 19 +++++++--- CHANGELOG.md | 14 ++++++++ Cargo.toml | 8 ++--- README.md | 2 +- src/de.rs | 40 ++++++++++----------- src/error.rs | 2 +- src/ser.rs | 4 +-- tests/test_custom_types.rs | 52 +++++++++++++--------------- tests/test_with_serde_path_to_err.rs | 10 +++--- 9 files changed, 87 insertions(+), 64 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5f21b46..f538d3d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,18 +4,29 @@ 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: - - name: Checkout repository - uses: actions/checkout@v4 + - 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: dtolnay/rust-toolchain@stable + - uses: rust-lang/crates-io-auth-action@v1 + id: auth - name: Publish to crates.io run: cargo publish env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 16b1080..a21bd55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## 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 diff --git a/Cargo.toml b/Cargo.toml index d636384..c0797e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "pythonize" -version = "0.25.0" +version = "0.26.0" authors = ["David Hewitt <1939362+davidhewitt@users.noreply.github.com>"] edition = "2021" -rust-version = "1.65" +rust-version = "1.74" license = "MIT" description = "Serde Serializer & Deserializer from Rust <--> Python, backed by PyO3." homepage = "https://github.com/davidhewitt/pythonize" @@ -13,11 +13,11 @@ documentation = "https://docs.rs/crate/pythonize/" [dependencies] serde = { version = "1.0", default-features = false, features = ["std"] } -pyo3 = { version = "0.25", default-features = false } +pyo3 = { version = "0.26", default-features = false } [dev-dependencies] serde = { version = "1.0", default-features = false, features = ["derive"] } -pyo3 = { version = "0.25", default-features = false, features = ["auto-initialize", "macros", "py-clone"] } +pyo3 = { version = "0.26", default-features = false, features = ["auto-initialize", "macros", "py-clone"] } serde_json = "1.0" serde_bytes = "0.11" maplit = "1.0.2" diff --git a/README.md b/README.md index 441f29f..2667523 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ let sample = Sample { bar: None }; -Python::with_gil(|py| { +Python::attach(|py| { // Rust -> Python let obj = pythonize(py, &sample).unwrap(); diff --git a/src/de.rs b/src/de.rs index 2860e48..a30dbca 100644 --- a/src/de.rs +++ b/src/de.rs @@ -24,7 +24,7 @@ impl<'a, 'py> Depythonizer<'a, 'py> { } fn sequence_access(&self, expected_len: Option) -> Result> { - let seq = self.input.downcast::()?; + let seq = self.input.cast::()?; let len = self.input.len()?; match expected_len { @@ -36,10 +36,10 @@ impl<'a, 'py> Depythonizer<'a, 'py> { } fn set_access(&self) -> Result> { - match self.input.downcast::() { + match self.input.cast::() { Ok(set) => Ok(PySetAsSequence::from_set(set)), Err(e) => { - if let Ok(f) = self.input.downcast::() { + if let Ok(f) = self.input.cast::() { Ok(PySetAsSequence::from_frozenset(f)) } else { Err(e.into()) @@ -49,7 +49,7 @@ impl<'a, 'py> Depythonizer<'a, 'py> { } fn dict_access(&self) -> Result> { - PyMappingAccess::new(self.input.downcast()?) + PyMappingAccess::new(self.input.cast()?) } fn deserialize_any_int<'de, V>(&self, int: &Bound<'_, PyInt>, visitor: V) -> Result @@ -111,7 +111,7 @@ impl<'de> de::Deserializer<'de> for &'_ mut Depythonizer<'_, '_> { self.deserialize_unit(visitor) } else if obj.is_instance_of::() { self.deserialize_bool(visitor) - } else if let Ok(x) = obj.downcast::() { + } 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) @@ -128,9 +128,9 @@ impl<'de> de::Deserializer<'de> for &'_ mut Depythonizer<'_, '_> { self.deserialize_f64(visitor) } else if obj.is_instance_of::() || obj.is_instance_of::() { self.deserialize_seq(visitor) - } else if obj.downcast::().is_ok() { + } else if obj.cast::().is_ok() { self.deserialize_tuple(obj.len()?, visitor) - } else if obj.downcast::().is_ok() { + } else if obj.cast::().is_ok() { self.deserialize_map(visitor) } else { Err(obj.get_type().qualname().map_or_else( @@ -151,7 +151,7 @@ impl<'de> de::Deserializer<'de> for &'_ mut Depythonizer<'_, '_> { where V: de::Visitor<'de>, { - let s = self.input.downcast::()?.to_cow()?; + let s = self.input.cast::()?.to_cow()?; if s.len() != 1 { return Err(PythonizeError::invalid_length_char()); } @@ -175,7 +175,7 @@ impl<'de> de::Deserializer<'de> for &'_ mut Depythonizer<'_, '_> { where V: de::Visitor<'de>, { - let s = self.input.downcast::()?; + let s = self.input.cast::()?; visitor.visit_str(&s.to_cow()?) } @@ -190,7 +190,7 @@ impl<'de> de::Deserializer<'de> for &'_ mut Depythonizer<'_, '_> { where V: de::Visitor<'de>, { - let b = self.input.downcast::()?; + let b = self.input.cast::()?; visitor.visit_bytes(b.as_bytes()) } @@ -303,9 +303,9 @@ impl<'de> de::Deserializer<'de> for &'_ mut Depythonizer<'_, '_> { V: de::Visitor<'de>, { let item = &self.input; - if let Ok(s) = item.downcast::() { + if let Ok(s) = item.cast::() { visitor.visit_enum(s.to_cow()?.into_deserializer()) - } else if let Ok(m) = item.downcast::() { + } 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()); @@ -313,7 +313,7 @@ impl<'de> de::Deserializer<'de> for &'_ mut Depythonizer<'_, '_> { let variant: Bound = m .keys()? .get_item(0)? - .downcast_into::() + .cast_into::() .map_err(|_| PythonizeError::dict_key_not_string())?; let value = m.get_item(&variant)?; visitor.visit_enum(PyEnumAccess::new(&value, variant)) @@ -328,7 +328,7 @@ impl<'de> de::Deserializer<'de> for &'_ mut Depythonizer<'_, '_> { { let s = self .input - .downcast::() + .cast::() .map_err(|_| PythonizeError::dict_key_not_string())?; visitor.visit_str(&s.to_cow()?) } @@ -528,7 +528,7 @@ mod test { where T: de::DeserializeOwned + PartialEq + std::fmt::Debug, { - Python::with_gil(|py| { + Python::attach(|py| { let obj = py.eval(code, None, None).unwrap(); let actual: T = depythonize(&obj).unwrap(); assert_eq!(&actual, expected); @@ -585,7 +585,7 @@ mod test { let code = c_str!("{'foo': 'Foo'}"); - Python::with_gil(|py| { + Python::attach(|py| { let locals = PyDict::new(py); let obj = py.eval(code, None, Some(&locals)).unwrap(); assert!(matches!( @@ -613,7 +613,7 @@ mod test { let code = c_str!("('cat', -10.05, 'foo')"); - Python::with_gil(|py| { + Python::attach(|py| { let locals = PyDict::new(py); let obj = py.eval(code, None, Some(&locals)).unwrap(); assert!(matches!( @@ -825,7 +825,7 @@ mod test { #[test] fn test_int_limits() { - Python::with_gil(|py| { + 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(); @@ -857,7 +857,7 @@ mod test { #[test] fn test_deserialize_bytes() { - Python::with_gil(|py| { + Python::attach(|py| { let obj = PyBytes::new(py, "hello".as_bytes()); let actual: Vec = depythonize(&obj).unwrap(); assert_eq!(actual, b"hello"); @@ -874,7 +874,7 @@ mod test { #[test] fn test_unknown_type() { - Python::with_gil(|py| { + Python::attach(|py| { let obj = py .import("decimal") .unwrap() diff --git a/src/error.rs b/src/error.rs index 1bcc556..7828a71 100644 --- a/src/error.rs +++ b/src/error.rs @@ -73,7 +73,7 @@ pub enum ErrorImpl { Message(String), /// A Python type not supported by the deserializer UnsupportedType(String), - /// A `PyAny` object that failed to downcast to an expected Python type + /// 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, diff --git a/src/ser.rs b/src/ser.rs index dce22e3..c8e6dd1 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -632,7 +632,7 @@ mod test { where T: Serialize, { - Python::with_gil(|py| -> PyResult<()> { + Python::attach(|py| -> PyResult<()> { let obj = pythonize(py, &src)?; let locals = PyDict::new(py); @@ -845,7 +845,7 @@ mod test { // serde treats &[u8] as a sequence of integers due to lack of specialization test_ser(b"foo", "[102,111,111]"); - Python::with_gil(|py| { + Python::attach(|py| { assert!(pythonize(py, serde_bytes::Bytes::new(b"foo")) .expect("bytes will always serialize successfully") .eq(&PyBytes::new(py, b"foo")) diff --git a/tests/test_custom_types.rs b/tests/test_custom_types.rs index 32c5768..27888d0 100644 --- a/tests/test_custom_types.rs +++ b/tests/test_custom_types.rs @@ -4,7 +4,7 @@ use pyo3::{ exceptions::{PyIndexError, PyKeyError}, prelude::*, types::{PyDict, PyMapping, PySequence, PyTuple}, - BoundObject, + IntoPyObjectExt, }; use pythonize::{ depythonize, pythonize_custom, PythonizeListType, PythonizeMappingType, @@ -15,7 +15,7 @@ use serde_json::{json, Value}; #[pyclass(sequence)] struct CustomList { - items: Vec, + items: Vec>, } #[pymethods] @@ -24,7 +24,7 @@ impl CustomList { self.items.len() } - fn __getitem__(&self, idx: isize) -> PyResult { + fn __getitem__(&self, idx: isize) -> PyResult> { self.items .get(idx as usize) .cloned() @@ -46,14 +46,12 @@ impl PythonizeListType for CustomList { CustomList { items: elements .into_iter() - .map(|item| item.into_pyobject(py).map(|x| x.into_any().unbind())) - .collect::, T::Error>>() - .map_err(Into::into)?, + .map(|item| item.into_py_any(py)) + .collect::>()?, }, - )? - .into_any(); + )?; - Ok(unsafe { sequence.downcast_into_unchecked() }) + Ok(unsafe { sequence.cast_into_unchecked() }) } } @@ -66,7 +64,7 @@ impl<'py> PythonizeTypes for PythonizeCustomList { #[test] fn test_custom_list() { - Python::with_gil(|py| { + Python::attach(|py| { PySequence::register::(py).unwrap(); let serialized = pythonize_custom::(py, &json!([1, 2, 3])).unwrap(); assert!(serialized.is_instance_of::()); @@ -78,7 +76,7 @@ fn test_custom_list() { #[pyclass(mapping)] struct CustomDict { - items: HashMap, + items: HashMap>, } #[pymethods] @@ -87,14 +85,14 @@ impl CustomDict { self.items.len() } - fn __getitem__(&self, key: String) -> PyResult { + 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: PyObject) { + fn __setitem__(&mut self, key: String, value: Py) { self.items.insert(key, value); } @@ -102,7 +100,7 @@ impl CustomDict { self.items.keys().collect() } - fn values(&self) -> Vec { + fn values(&self) -> Vec> { self.items.values().cloned().collect() } } @@ -124,11 +122,11 @@ impl PythonizeMappingType for CustomDict { key: Bound<'py, PyAny>, value: Bound<'py, PyAny>, ) -> PyResult<()> { - unsafe { builder.downcast_unchecked::() }.set_item(key, value) + unsafe { builder.cast_unchecked::() }.set_item(key, value) } fn finish<'py>(builder: Self::Builder<'py>) -> PyResult> { - Ok(unsafe { builder.into_any().downcast_into_unchecked() }) + Ok(unsafe { builder.cast_into_unchecked() }) } } @@ -141,7 +139,7 @@ impl<'py> PythonizeTypes for PythonizeCustomDict { #[test] fn test_custom_dict() { - Python::with_gil(|py| { + Python::attach(|py| { PyMapping::register::(py).unwrap(); let serialized = pythonize_custom::(py, &json!({ "hello": 1, "world": 2 })) @@ -155,7 +153,7 @@ fn test_custom_dict() { #[test] fn test_tuple() { - Python::with_gil(|py| { + Python::attach(|py| { PyMapping::register::(py).unwrap(); let serialized = pythonize_custom::(py, &json!([1, 2, 3, 4])).unwrap(); @@ -169,7 +167,7 @@ fn test_tuple() { #[test] fn test_pythonizer_can_be_created() { // https://github.com/davidhewitt/pythonize/pull/56 - Python::with_gil(|py| { + Python::attach(|py| { let sample = json!({ "hello": 1, "world": 2 }); assert!(sample .serialize(Pythonizer::new(py)) @@ -186,7 +184,7 @@ fn test_pythonizer_can_be_created() { #[pyclass(mapping)] struct NamedCustomDict { name: String, - items: HashMap, + items: HashMap>, } #[pymethods] @@ -195,14 +193,14 @@ impl NamedCustomDict { self.items.len() } - fn __getitem__(&self, key: String) -> PyResult { + 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: PyObject) { + fn __setitem__(&mut self, key: String, value: Py) { self.items.insert(key, value); } @@ -210,7 +208,7 @@ impl NamedCustomDict { self.items.keys().collect() } - fn values(&self) -> Vec { + fn values(&self) -> Vec> { self.items.values().cloned().collect() } } @@ -237,11 +235,11 @@ impl PythonizeNamedMappingType for NamedCustomDict { name: Bound<'py, pyo3::types::PyString>, value: Bound<'py, PyAny>, ) -> PyResult<()> { - unsafe { builder.downcast_unchecked::() }.set_item(name, value) + unsafe { builder.cast_unchecked::() }.set_item(name, value) } fn finish<'py>(builder: Self::Builder<'py>) -> PyResult> { - Ok(unsafe { builder.into_any().downcast_into_unchecked() }) + Ok(unsafe { builder.cast_into_unchecked() }) } } @@ -260,7 +258,7 @@ struct Struct { #[test] fn test_custom_unnamed_dict() { - Python::with_gil(|py| { + Python::attach(|py| { PyMapping::register::(py).unwrap(); let serialized = pythonize_custom::(py, &Struct { hello: 1, world: 2 }).unwrap(); @@ -273,7 +271,7 @@ fn test_custom_unnamed_dict() { #[test] fn test_custom_named_dict() { - Python::with_gil(|py| { + Python::attach(|py| { PyMapping::register::(py).unwrap(); let serialized = pythonize_custom::(py, &Struct { hello: 1, world: 2 }) diff --git a/tests/test_with_serde_path_to_err.rs b/tests/test_with_serde_path_to_err.rs index 9c3b688..d92c4d1 100644 --- a/tests/test_with_serde_path_to_err.rs +++ b/tests/test_with_serde_path_to_err.rs @@ -40,7 +40,7 @@ impl Serialize for CannotSerialize { #[test] fn test_de_valid() { - Python::with_gil(|py| { + Python::attach(|py| { let pyroot = PyDict::new(py); pyroot.set_item("root_key", "root_value").unwrap(); @@ -82,7 +82,7 @@ fn test_de_valid() { #[test] fn test_de_invalid() { - Python::with_gil(|py| { + Python::attach(|py| { let pyroot = PyDict::new(py); pyroot.set_item("root_key", "root_value").unwrap(); @@ -106,7 +106,7 @@ fn test_de_invalid() { #[test] fn test_ser_valid() { - Python::with_gil(|py| { + Python::attach(|py| { let root = Root { root_key: String::from("root_value"), root_map: BTreeMap::from([ @@ -128,7 +128,7 @@ fn test_ser_valid() { let ser = pythonize::Pythonizer::>::from(py); let pyroot: Bound<'_, PyAny> = serde_path_to_error::serialize(&root, ser).unwrap(); - let pyroot = pyroot.downcast::().unwrap(); + let pyroot = pyroot.cast::().unwrap(); assert_eq!(pyroot.len(), 2); let root_value: String = pyroot @@ -181,7 +181,7 @@ fn test_ser_valid() { #[test] fn test_ser_invalid() { - Python::with_gil(|py| { + Python::attach(|py| { let root = Root { root_key: String::from("root_value"), root_map: BTreeMap::from([ From 9d6d9287693032b869909bfe8dd07a8341749dd9 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 7 Nov 2025 21:50:18 +0000 Subject: [PATCH 08/10] ci: extend test matrix to 3.14, more arm runners (#98) --- .github/workflows/ci.yml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c08531..3d69440 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,17 +49,21 @@ jobs: strategy: fail-fast: false # If one platform fails, allow the rest to keep testing. matrix: - python-architecture: ["x64"] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.13t"] - os: ["macos-13", "ubuntu-latest", "windows-latest"] + 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.13" + - python-version: "3.14" os: "ubuntu-latest" rust: ${{ needs.resolve.outputs.MSRV }} - - python-version: "3.13" - python-architecture: "arm64" - os: "macos-latest" + - 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: @@ -69,7 +73,6 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - architecture: ${{ matrix.python-python-architecture }} - name: Install Rust toolchain uses: dtolnay/rust-toolchain@master @@ -88,9 +91,7 @@ jobs: - name: Test run: cargo test --verbose - # https://github.com/PyO3/pyo3/issues/4709 - can't use abi3 w. freethreaded build - - if: ${{ !endsWith(matrix.python-version, 't') }} - name: Test (abi3) + - name: Test (abi3) run: cargo test --verbose --features pyo3/abi3-py37 env: From 89420b2d91721f277f073befa70a95016606e9ed Mon Sep 17 00:00:00 2001 From: Tino Wagner Date: Fri, 7 Nov 2025 22:50:36 +0100 Subject: [PATCH 09/10] Bump PyO3 to 0.27 (#96) * Bump PyO3 to 0.27 Update pyo3 dependency to 0.27. Replace deprecated DowncastError with CastError. --------- Co-authored-by: David Hewitt --- Cargo.toml | 4 ++-- src/error.rs | 14 +++++++------- tests/test_with_serde_path_to_err.rs | 11 +++++++---- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c0797e7..ce406ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,11 +13,11 @@ documentation = "https://docs.rs/crate/pythonize/" [dependencies] serde = { version = "1.0", default-features = false, features = ["std"] } -pyo3 = { version = "0.26", default-features = false } +pyo3 = { version = "0.27", default-features = false } [dev-dependencies] serde = { version = "1.0", default-features = false, features = ["derive"] } -pyo3 = { version = "0.26", default-features = false, features = ["auto-initialize", "macros", "py-clone"] } +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" diff --git a/src/error.rs b/src/error.rs index 7828a71..b608106 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,5 @@ use pyo3::PyErr; -use pyo3::{exceptions::*, DowncastError, DowncastIntoError}; +use pyo3::{exceptions::*, CastError, CastIntoError}; use serde::{de, ser}; use std::convert::Infallible; use std::error; @@ -153,18 +153,18 @@ impl From for PythonizeError { } } -/// Handle errors that occur when attempting to use `PyAny::cast_as` -impl<'a, 'py> From> for PythonizeError { - fn from(other: DowncastError<'a, 'py>) -> Self { +/// 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_as` -impl<'py> From> for PythonizeError { - fn from(other: DowncastIntoError<'py>) -> Self { +/// 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())), } diff --git a/tests/test_with_serde_path_to_err.rs b/tests/test_with_serde_path_to_err.rs index d92c4d1..82fd8bb 100644 --- a/tests/test_with_serde_path_to_err.rs +++ b/tests/test_with_serde_path_to_err.rs @@ -100,7 +100,10 @@ fn test_de_invalid() { 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 converted to 'PyString'"); + assert_eq!( + err.to_string(), + "root_map.nested_1.nested_key: unexpected type: 'int' object cannot be cast as 'str'" + ); }) } @@ -143,7 +146,7 @@ fn test_ser_valid() { .get_item("root_map") .unwrap() .unwrap() - .downcast_into::() + .cast_into::() .unwrap(); assert_eq!(root_map.len(), 2); @@ -151,7 +154,7 @@ fn test_ser_valid() { .get_item("nested_0") .unwrap() .unwrap() - .downcast_into::() + .cast_into::() .unwrap(); assert_eq!(nested_0.len(), 1); let nested_key_0: String = nested_0 @@ -166,7 +169,7 @@ fn test_ser_valid() { .get_item("nested_1") .unwrap() .unwrap() - .downcast_into::() + .cast_into::() .unwrap(); assert_eq!(nested_1.len(), 1); let nested_key_1: String = nested_1 From 43c714f13d40db35b080659c67b818758cc8c202 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 7 Nov 2025 21:57:31 +0000 Subject: [PATCH 10/10] release: 0.27.0 (#99) --- CHANGELOG.md | 3 +++ Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a21bd55..f267740 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 0.27.0 - 2025-11-07 +- Update to PyO3 0.27 + ## 0.26.0 - 2025-08-30 ### Packaging diff --git a/Cargo.toml b/Cargo.toml index ce406ad..4a714a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pythonize" -version = "0.26.0" +version = "0.27.0" authors = ["David Hewitt <1939362+davidhewitt@users.noreply.github.com>"] edition = "2021" rust-version = "1.74"