Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[package]
name = "pythonize"
version = "0.25.0"
version = "0.26.0"
authors = ["David Hewitt <[email protected]>"]
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"
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ let sample = Sample {
bar: None
};

Python::with_gil(|py| {
Python::attach(|py| {
// Rust -> Python
let obj = pythonize(py, &sample).unwrap();

Expand Down
40 changes: 20 additions & 20 deletions src/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ impl<'a, 'py> Depythonizer<'a, 'py> {
}

fn sequence_access(&self, expected_len: Option<usize>) -> Result<PySequenceAccess<'a, 'py>> {
let seq = self.input.downcast::<PySequence>()?;
let seq = self.input.cast::<PySequence>()?;
let len = self.input.len()?;

match expected_len {
Expand All @@ -36,10 +36,10 @@ impl<'a, 'py> Depythonizer<'a, 'py> {
}

fn set_access(&self) -> Result<PySetAsSequence<'py>> {
match self.input.downcast::<PySet>() {
match self.input.cast::<PySet>() {
Ok(set) => Ok(PySetAsSequence::from_set(set)),
Err(e) => {
if let Ok(f) = self.input.downcast::<PyFrozenSet>() {
if let Ok(f) = self.input.cast::<PyFrozenSet>() {
Ok(PySetAsSequence::from_frozenset(f))
} else {
Err(e.into())
Expand All @@ -49,7 +49,7 @@ impl<'a, 'py> Depythonizer<'a, 'py> {
}

fn dict_access(&self) -> Result<PyMappingAccess<'py>> {
PyMappingAccess::new(self.input.downcast()?)
PyMappingAccess::new(self.input.cast()?)
}

fn deserialize_any_int<'de, V>(&self, int: &Bound<'_, PyInt>, visitor: V) -> Result<V::Value>
Expand Down Expand Up @@ -111,7 +111,7 @@ impl<'de> de::Deserializer<'de> for &'_ mut Depythonizer<'_, '_> {
self.deserialize_unit(visitor)
} else if obj.is_instance_of::<PyBool>() {
self.deserialize_bool(visitor)
} else if let Ok(x) = obj.downcast::<PyInt>() {
} else if let Ok(x) = obj.cast::<PyInt>() {
self.deserialize_any_int(x, visitor)
} else if obj.is_instance_of::<PyList>() || obj.is_instance_of::<PyTuple>() {
self.deserialize_tuple(obj.len()?, visitor)
Expand All @@ -128,9 +128,9 @@ impl<'de> de::Deserializer<'de> for &'_ mut Depythonizer<'_, '_> {
self.deserialize_f64(visitor)
} else if obj.is_instance_of::<PyFrozenSet>() || obj.is_instance_of::<PySet>() {
self.deserialize_seq(visitor)
} else if obj.downcast::<PySequence>().is_ok() {
} else if obj.cast::<PySequence>().is_ok() {
self.deserialize_tuple(obj.len()?, visitor)
} else if obj.downcast::<PyMapping>().is_ok() {
} else if obj.cast::<PyMapping>().is_ok() {
self.deserialize_map(visitor)
} else {
Err(obj.get_type().qualname().map_or_else(
Expand All @@ -151,7 +151,7 @@ impl<'de> de::Deserializer<'de> for &'_ mut Depythonizer<'_, '_> {
where
V: de::Visitor<'de>,
{
let s = self.input.downcast::<PyString>()?.to_cow()?;
let s = self.input.cast::<PyString>()?.to_cow()?;
if s.len() != 1 {
return Err(PythonizeError::invalid_length_char());
}
Expand All @@ -175,7 +175,7 @@ impl<'de> de::Deserializer<'de> for &'_ mut Depythonizer<'_, '_> {
where
V: de::Visitor<'de>,
{
let s = self.input.downcast::<PyString>()?;
let s = self.input.cast::<PyString>()?;
visitor.visit_str(&s.to_cow()?)
}

Expand All @@ -190,7 +190,7 @@ impl<'de> de::Deserializer<'de> for &'_ mut Depythonizer<'_, '_> {
where
V: de::Visitor<'de>,
{
let b = self.input.downcast::<PyBytes>()?;
let b = self.input.cast::<PyBytes>()?;
visitor.visit_bytes(b.as_bytes())
}

Expand Down Expand Up @@ -303,17 +303,17 @@ impl<'de> de::Deserializer<'de> for &'_ mut Depythonizer<'_, '_> {
V: de::Visitor<'de>,
{
let item = &self.input;
if let Ok(s) = item.downcast::<PyString>() {
if let Ok(s) = item.cast::<PyString>() {
visitor.visit_enum(s.to_cow()?.into_deserializer())
} else if let Ok(m) = item.downcast::<PyMapping>() {
} else if let Ok(m) = item.cast::<PyMapping>() {
// Get the enum variant from the mapping key
if m.len()? != 1 {
return Err(PythonizeError::invalid_length_enum());
}
let variant: Bound<PyString> = m
.keys()?
.get_item(0)?
.downcast_into::<PyString>()
.cast_into::<PyString>()
.map_err(|_| PythonizeError::dict_key_not_string())?;
let value = m.get_item(&variant)?;
visitor.visit_enum(PyEnumAccess::new(&value, variant))
Expand All @@ -328,7 +328,7 @@ impl<'de> de::Deserializer<'de> for &'_ mut Depythonizer<'_, '_> {
{
let s = self
.input
.downcast::<PyString>()
.cast::<PyString>()
.map_err(|_| PythonizeError::dict_key_not_string())?;
visitor.visit_str(&s.to_cow()?)
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<u8> = depythonize(&obj).unwrap();
assert_eq!(actual, b"hello");
Expand All @@ -874,7 +874,7 @@ mod test {

#[test]
fn test_unknown_type() {
Python::with_gil(|py| {
Python::attach(|py| {
let obj = py
.import("decimal")
.unwrap()
Expand Down
2 changes: 1 addition & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/ser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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"))
Expand Down
Loading
Loading