From bfb72fb8e93a58c3e6e9ef343ee6ee2b50a52a9c Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Wed, 18 Jun 2025 23:00:22 +0200 Subject: [PATCH 1/7] Add `PyString::from_fmt` using new `PyUnicodeWriter` --- pyo3-ffi/src/compat/py_3_14.rs | 80 +++++++++++++++ pyo3-ffi/src/cpython/unicodeobject.rs | 71 +++++++++++++ src/fmt.rs | 137 ++++++++++++++++++++++++++ src/lib.rs | 1 + src/types/string.rs | 22 ++++- 5 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 src/fmt.rs diff --git a/pyo3-ffi/src/compat/py_3_14.rs b/pyo3-ffi/src/compat/py_3_14.rs index 6fdaef17488..1f70b7c2c83 100644 --- a/pyo3-ffi/src/compat/py_3_14.rs +++ b/pyo3-ffi/src/compat/py_3_14.rs @@ -24,3 +24,83 @@ compat_function!( } } ); + +compat_function!( + originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); + + pub unsafe fn PyUnicodeWriter_Create(length: crate::Py_ssize_t) -> *mut crate::PyUnicodeWriter { + if length < 0 { + crate::PyErr_SetString( + crate::PyExc_ValueError, + c_str!("length must be positive").as_ptr(), + ); + return std::ptr::null_mut(); + } + + let size = std::mem::size_of::(); + let writer: *mut crate::_PyUnicodeWriter = crate::PyMem_Malloc(size).cast(); + crate::_PyUnicodeWriter_Init(writer); + if crate::_PyUnicodeWriter_Prepare(writer, length, 127) < 0 { + PyUnicodeWriter_Discard(writer.cast()); + return std::ptr::null_mut(); + } + (*writer).overallocate = 1; + writer.cast() + } +); + +compat_function!( + originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); + + pub unsafe fn PyUnicodeWriter_Finish(writer: *mut crate::PyUnicodeWriter) -> *mut crate::PyObject { + let str = crate::_PyUnicodeWriter_Finish(writer.cast()); + crate::PyMem_Free(writer.cast()); + str + } +); + +compat_function!( + originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); + + pub unsafe fn PyUnicodeWriter_Discard(writer: *mut crate::PyUnicodeWriter) -> () { + crate::_PyUnicodeWriter_Dealloc(writer.cast()); + crate::PyMem_Free(writer.cast()) + } +); + +compat_function!( + originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); + + pub unsafe fn PyUnicodeWriter_WriteChar(writer: *mut crate::PyUnicodeWriter, ch: crate::Py_UCS4) -> std::os::raw::c_int { + if ch > 0x10ffff { + crate::PyErr_SetString( + crate::PyExc_ValueError, + c_str!("character must be in range(0x110000)").as_ptr(), + ); + return -1; + } + + crate::_PyUnicodeWriter_WriteChar(writer.cast(), ch) + } +); + +compat_function!( + originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); + + pub unsafe fn PyUnicodeWriter_WriteUTF8(writer: *mut crate::PyUnicodeWriter,str: *const std::os::raw::c_char, size: crate::Py_ssize_t) -> std::os::raw::c_int { + let size = if size < 0 { + libc::strlen(str) as isize + } else { + size + }; + + let py_str = crate::PyUnicode_FromStringAndSize(str, size); + if py_str.is_null() { + return -1; + } + + let result = crate::_PyUnicodeWriter_WriteStr(writer.cast(), py_str); + crate::Py_DECREF(py_str); + result + } +); diff --git a/pyo3-ffi/src/cpython/unicodeobject.rs b/pyo3-ffi/src/cpython/unicodeobject.rs index 452c82e4c4b..d19a35d6106 100644 --- a/pyo3-ffi/src/cpython/unicodeobject.rs +++ b/pyo3-ffi/src/cpython/unicodeobject.rs @@ -686,6 +686,77 @@ extern "C" { // skipped PyUnicode_GetMax } +opaque_struct!(pub PyUnicodeWriter); + +#[cfg(not(Py_3_14))] +#[repr(C)] +pub(crate) struct _PyUnicodeWriter { + buffer: *mut PyObject, + data: *mut c_void, + kind: c_int, + pub(crate) maxchar: Py_UCS4, + pub(crate) size: Py_ssize_t, + pub(crate) pos: Py_ssize_t, + min_length: Py_ssize_t, + min_char: Py_UCS4, + pub(crate) overallocate: c_char, + readonly: c_char, +} + +extern "C" { + #[cfg(Py_3_14)] + pub fn PyUnicodeWriter_Create(length: Py_ssize_t) -> *mut PyUnicodeWriter; + #[cfg(Py_3_14)] + pub fn PyUnicodeWriter_Finish(writer: *mut PyUnicodeWriter) -> *mut PyObject; + #[cfg(not(Py_3_14))] + pub(crate) fn _PyUnicodeWriter_Finish(writer: *mut _PyUnicodeWriter) -> *mut PyObject; + #[cfg(Py_3_14)] + pub fn PyUnicodeWriter_Discard(writer: *mut PyUnicodeWriter); + #[cfg(not(Py_3_14))] + pub(crate) fn _PyUnicodeWriter_Dealloc(writer: *mut _PyUnicodeWriter); + #[cfg(not(Py_3_14))] + pub(crate) fn _PyUnicodeWriter_Init(writer: *mut _PyUnicodeWriter); + #[cfg(not(Py_3_14))] + pub(crate) fn _PyUnicodeWriter_PrepareInternal( + writer: *mut _PyUnicodeWriter, + length: Py_ssize_t, + maxchars: Py_UCS4, + ) -> c_int; + #[cfg(Py_3_14)] + pub fn PyUnicodeWriter_WriteChar(writer: *mut PyUnicodeWriter, ch: Py_UCS4) -> c_int; + #[cfg(not(Py_3_14))] + pub(crate) fn _PyUnicodeWriter_WriteChar(writer: *mut _PyUnicodeWriter, ch: Py_UCS4) -> c_int; + #[cfg(not(Py_3_14))] + pub(crate) fn _PyUnicodeWriter_WriteStr( + writer: *mut _PyUnicodeWriter, + str: *mut PyObject, + ) -> c_int; + #[cfg(Py_3_14)] + pub fn PyUnicodeWriter_WriteUTF8( + writer: *mut PyUnicodeWriter, + str: *const c_char, + size: Py_ssize_t, + ) -> c_int; +} + +#[cfg(not(Py_3_14))] +#[inline(always)] +pub(crate) unsafe fn _PyUnicodeWriter_Prepare( + writer: *mut _PyUnicodeWriter, + length: Py_ssize_t, + maxchars: Py_UCS4, +) -> c_int { + if maxchars <= (*writer).maxchar && length <= (*writer).size - (*writer).pos { + return 0; + } + + if length == 0 { + return 0; + } + + _PyUnicodeWriter_PrepareInternal(writer, length, maxchars) +} + // skipped _PyUnicodeWriter // skipped _PyUnicodeWriter_Init // skipped _PyUnicodeWriter_Prepare diff --git a/src/fmt.rs b/src/fmt.rs new file mode 100644 index 00000000000..213d468e48a --- /dev/null +++ b/src/fmt.rs @@ -0,0 +1,137 @@ +//! This module provides the `PyUnicodeWriter` struct, which is a utility for efficiently +//! constructing Python strings using Rust's `fmt::Write` trait. +//! It allows for incremental string construction, without the need for repeated allocations, and +//! is particularly useful for building strings in a performance-sensitive context. +use crate::ffi::compat::{ + PyUnicodeWriter_Create, PyUnicodeWriter_Discard, PyUnicodeWriter_Finish, + PyUnicodeWriter_WriteChar, PyUnicodeWriter_WriteUTF8, +}; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::impl_::callback::WrappingCastTo; +use crate::types::{PyAnyMethods, PyString}; +use crate::{ffi, Bound, PyErr, PyResult, Python}; +use std::ptr::NonNull; +use std::{fmt, mem}; + +/// The `PyUnicodeWriter` is a utility for efficiently constructing Python strings +pub struct PyUnicodeWriter { + writer: NonNull, + last_error: Option, +} + +impl PyUnicodeWriter { + /// Creates a new `PyUnicodeWriter`. + pub fn new(py: Python<'_>) -> PyResult { + Self::with_capacity(py, 0) + } + + /// Creates a new `PyUnicodeWriter` with the specified initial capacity. + pub fn with_capacity(py: Python<'_>, capacity: usize) -> PyResult { + match NonNull::new(unsafe { PyUnicodeWriter_Create(capacity.wrapping_cast()) }) { + Some(ptr) => Ok(PyUnicodeWriter { + writer: ptr, + last_error: None, + }), + None => Err(PyErr::fetch(py)), + } + } + + /// Consumes the `PyUnicodeWriter` and returns a `Bound` containing the constructed string. + pub fn into_py_string(self, py: Python<'_>) -> PyResult> { + let writer_ptr = self.as_ptr(); + mem::forget(self); + Ok(unsafe { + PyUnicodeWriter_Finish(writer_ptr) + .assume_owned_or_err(py)? + .downcast_into_unchecked() + }) + } + + /// When fmt::Write returned an error, this function can be used to retrieve the last error that occurred. + pub fn take_error(&mut self) -> Option { + self.last_error.take() + } + + fn as_ptr(&self) -> *mut ffi::PyUnicodeWriter { + self.writer.as_ptr() + } + + fn set_error(&mut self) { + Python::with_gil(|py| { + self.last_error = Some(PyErr::fetch(py)); + }) + } +} + +impl fmt::Write for PyUnicodeWriter { + fn write_str(&mut self, s: &str) -> fmt::Result { + let result = unsafe { + PyUnicodeWriter_WriteUTF8(self.as_ptr(), s.as_ptr().cast(), s.len() as isize) + }; + if result < 0 { + self.set_error(); + Err(fmt::Error) + } else { + Ok(()) + } + } + + fn write_char(&mut self, c: char) -> fmt::Result { + let result = unsafe { PyUnicodeWriter_WriteChar(self.as_ptr(), c as u32) }; + if result < 0 { + self.set_error(); + Err(fmt::Error) + } else { + Ok(()) + } + } +} + +impl Drop for PyUnicodeWriter { + fn drop(&mut self) { + unsafe { + PyUnicodeWriter_Discard(self.as_ptr()); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::PyStringMethods; + use crate::{IntoPyObject, Python}; + use std::fmt::Write; + + #[test] + fn unicode_writer_test() { + Python::with_gil(|py| { + let mut writer = PyUnicodeWriter::new(py).unwrap(); + write!(writer, "Hello {}!", "world").unwrap(); + writer.write_char('😎').unwrap(); + let result = writer.into_py_string(py).unwrap(); + assert_eq!(result.to_string(), "Hello world!😎"); + }); + } + + #[test] + fn test_pystring_from_fmt() { + Python::with_gil(|py| { + PyString::from_fmt(py, format_args!("Hello {}!", "world")).unwrap(); + }); + } + + #[test] + fn test_complex_format() { + Python::with_gil(|py| { + let complex_value = (42, "foo", 3.14).into_pyobject(py).unwrap(); + let py_string = PyString::from_fmt( + py, + format_args!("This is some complex value: {complex_value}"), + ) + .unwrap(); + let actual = py_string.to_cow().unwrap(); + let expected = "This is some complex value: (42, 'foo', 3.14)"; + assert_eq!(actual, expected); + }); + } +} diff --git a/src/lib.rs b/src/lib.rs index 8d684b67c18..8aedcc61b15 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -421,6 +421,7 @@ pub mod coroutine; mod err; pub mod exceptions; pub mod ffi; +pub mod fmt; mod gil; #[doc(hidden)] pub mod impl_; diff --git a/src/types/string.rs b/src/types/string.rs index 774624df108..4654deaa3c9 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -1,6 +1,7 @@ #[cfg(not(Py_LIMITED_API))] use crate::exceptions::PyUnicodeDecodeError; use crate::ffi_ptr_ext::FfiPtrExt; +use crate::fmt::PyUnicodeWriter; use crate::instance::Borrowed; use crate::py_result_ext::PyResultExt; use crate::types::any::PyAnyMethods; @@ -9,7 +10,8 @@ use crate::types::PyBytes; use crate::{ffi, Bound, Py, PyAny, PyResult, Python}; use std::borrow::Cow; use std::ffi::CString; -use std::str; +use std::fmt::Write as _; +use std::{fmt, str}; /// Represents raw data backing a Python `str`. /// @@ -209,6 +211,24 @@ impl PyString { .downcast_into_unchecked() } } + + /// Creates a Python string using a format string. + /// + /// This function is similar to [`format!`], but it returns a Python string object instead of a Rust string. + pub fn from_fmt<'py>( + py: Python<'py>, + args: fmt::Arguments<'_>, + ) -> PyResult> { + if let Some(static_string) = args.as_str() { + return Ok(PyString::new(py, static_string)); + }; + + let mut writer = PyUnicodeWriter::new(py)?; + writer + .write_fmt(args) + .map_err(|_| writer.take_error().expect("expected error"))?; + writer.into_py_string(py) + } } /// Implementation of functionality for [`PyString`]. From eb46ca24792a79aac69c529589f94ea7528bd388 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 19 Jun 2025 15:01:39 +0200 Subject: [PATCH 2/7] Make `PyUnicodeWriter` an alias on `<=3.13` --- pyo3-ffi/src/cpython/unicodeobject.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyo3-ffi/src/cpython/unicodeobject.rs b/pyo3-ffi/src/cpython/unicodeobject.rs index d19a35d6106..46c34e61db0 100644 --- a/pyo3-ffi/src/cpython/unicodeobject.rs +++ b/pyo3-ffi/src/cpython/unicodeobject.rs @@ -686,11 +686,16 @@ extern "C" { // skipped PyUnicode_GetMax } +#[cfg(Py_3_14)] opaque_struct!(pub PyUnicodeWriter); #[cfg(not(Py_3_14))] +pub type PyUnicodeWriter = _PyUnicodeWriter; + +#[cfg(not(Py_3_14))] +#[doc(hidden)] #[repr(C)] -pub(crate) struct _PyUnicodeWriter { +pub struct _PyUnicodeWriter { buffer: *mut PyObject, data: *mut c_void, kind: c_int, From 4ee8e0d8d330d94cdb8a07c83553493aa3925216 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 19 Jun 2025 15:29:00 +0200 Subject: [PATCH 3/7] Make compatible with abi3 --- pyo3-ffi/src/compat/py_3_14.rs | 5 ++++ src/fmt.rs | 45 ++++++++++++++++++++++------------ src/types/string.rs | 20 +++++++++++---- 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/pyo3-ffi/src/compat/py_3_14.rs b/pyo3-ffi/src/compat/py_3_14.rs index 1f70b7c2c83..f2cf0d0c143 100644 --- a/pyo3-ffi/src/compat/py_3_14.rs +++ b/pyo3-ffi/src/compat/py_3_14.rs @@ -28,6 +28,7 @@ compat_function!( compat_function!( originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); + #[cfg(not(Py_LIMITED_API))] pub unsafe fn PyUnicodeWriter_Create(length: crate::Py_ssize_t) -> *mut crate::PyUnicodeWriter { if length < 0 { crate::PyErr_SetString( @@ -52,6 +53,7 @@ compat_function!( compat_function!( originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); + #[cfg(not(Py_LIMITED_API))] pub unsafe fn PyUnicodeWriter_Finish(writer: *mut crate::PyUnicodeWriter) -> *mut crate::PyObject { let str = crate::_PyUnicodeWriter_Finish(writer.cast()); crate::PyMem_Free(writer.cast()); @@ -62,6 +64,7 @@ compat_function!( compat_function!( originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); + #[cfg(not(Py_LIMITED_API))] pub unsafe fn PyUnicodeWriter_Discard(writer: *mut crate::PyUnicodeWriter) -> () { crate::_PyUnicodeWriter_Dealloc(writer.cast()); crate::PyMem_Free(writer.cast()) @@ -71,6 +74,7 @@ compat_function!( compat_function!( originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); + #[cfg(not(Py_LIMITED_API))] pub unsafe fn PyUnicodeWriter_WriteChar(writer: *mut crate::PyUnicodeWriter, ch: crate::Py_UCS4) -> std::os::raw::c_int { if ch > 0x10ffff { crate::PyErr_SetString( @@ -87,6 +91,7 @@ compat_function!( compat_function!( originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); + #[cfg(not(Py_LIMITED_API))] pub unsafe fn PyUnicodeWriter_WriteUTF8(writer: *mut crate::PyUnicodeWriter,str: *const std::os::raw::c_char, size: crate::Py_ssize_t) -> std::os::raw::c_int { let size = if size < 0 { libc::strlen(str) as isize diff --git a/src/fmt.rs b/src/fmt.rs index 213d468e48a..0c078a09092 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -2,23 +2,36 @@ //! constructing Python strings using Rust's `fmt::Write` trait. //! It allows for incremental string construction, without the need for repeated allocations, and //! is particularly useful for building strings in a performance-sensitive context. -use crate::ffi::compat::{ - PyUnicodeWriter_Create, PyUnicodeWriter_Discard, PyUnicodeWriter_Finish, - PyUnicodeWriter_WriteChar, PyUnicodeWriter_WriteUTF8, +#[cfg(not(Py_LIMITED_API))] +use { + crate::ffi::compat::{ + PyUnicodeWriter_Create, PyUnicodeWriter_Discard, PyUnicodeWriter_Finish, + PyUnicodeWriter_WriteChar, PyUnicodeWriter_WriteUTF8, + }, + crate::ffi_ptr_ext::FfiPtrExt, + crate::impl_::callback::WrappingCastTo, + crate::types::{PyAnyMethods, PyString}, + crate::{ffi, Bound, PyErr, PyResult, Python}, + std::ptr::NonNull, + std::{fmt, mem}, }; -use crate::ffi_ptr_ext::FfiPtrExt; -use crate::impl_::callback::WrappingCastTo; -use crate::types::{PyAnyMethods, PyString}; -use crate::{ffi, Bound, PyErr, PyResult, Python}; -use std::ptr::NonNull; -use std::{fmt, mem}; +/// This is like the `format!` macro, but it returns a `PyString` instead of a `String`. +#[macro_export] +macro_rules! py_format { + ($py: expr, $($arg:tt)*) => { + $crate::types::PyString::from_fmt($py, format_args!($($arg)*)) + } +} + +#[cfg(not(Py_LIMITED_API))] /// The `PyUnicodeWriter` is a utility for efficiently constructing Python strings pub struct PyUnicodeWriter { writer: NonNull, last_error: Option, } +#[cfg(not(Py_LIMITED_API))] impl PyUnicodeWriter { /// Creates a new `PyUnicodeWriter`. pub fn new(py: Python<'_>) -> PyResult { @@ -63,6 +76,7 @@ impl PyUnicodeWriter { } } +#[cfg(not(Py_LIMITED_API))] impl fmt::Write for PyUnicodeWriter { fn write_str(&mut self, s: &str) -> fmt::Result { let result = unsafe { @@ -87,6 +101,7 @@ impl fmt::Write for PyUnicodeWriter { } } +#[cfg(not(Py_LIMITED_API))] impl Drop for PyUnicodeWriter { fn drop(&mut self) { unsafe { @@ -97,13 +112,15 @@ impl Drop for PyUnicodeWriter { #[cfg(test)] mod tests { + #[cfg(not(Py_LIMITED_API))] use super::*; use crate::types::PyStringMethods; use crate::{IntoPyObject, Python}; - use std::fmt::Write; #[test] + #[cfg(not(Py_LIMITED_API))] fn unicode_writer_test() { + use std::fmt::Write; Python::with_gil(|py| { let mut writer = PyUnicodeWriter::new(py).unwrap(); write!(writer, "Hello {}!", "world").unwrap(); @@ -116,7 +133,7 @@ mod tests { #[test] fn test_pystring_from_fmt() { Python::with_gil(|py| { - PyString::from_fmt(py, format_args!("Hello {}!", "world")).unwrap(); + py_format!(py, "Hello {}!", "world").unwrap(); }); } @@ -124,11 +141,7 @@ mod tests { fn test_complex_format() { Python::with_gil(|py| { let complex_value = (42, "foo", 3.14).into_pyobject(py).unwrap(); - let py_string = PyString::from_fmt( - py, - format_args!("This is some complex value: {complex_value}"), - ) - .unwrap(); + let py_string = py_format!(py, "This is some complex value: {complex_value}").unwrap(); let actual = py_string.to_cow().unwrap(); let expected = "This is some complex value: (42, 'foo', 3.14)"; assert_eq!(actual, expected); diff --git a/src/types/string.rs b/src/types/string.rs index 4654deaa3c9..b0bfa94894c 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -1,6 +1,7 @@ #[cfg(not(Py_LIMITED_API))] use crate::exceptions::PyUnicodeDecodeError; use crate::ffi_ptr_ext::FfiPtrExt; +#[cfg(not(Py_LIMITED_API))] use crate::fmt::PyUnicodeWriter; use crate::instance::Borrowed; use crate::py_result_ext::PyResultExt; @@ -10,6 +11,7 @@ use crate::types::PyBytes; use crate::{ffi, Bound, Py, PyAny, PyResult, Python}; use std::borrow::Cow; use std::ffi::CString; +#[cfg(not(Py_LIMITED_API))] use std::fmt::Write as _; use std::{fmt, str}; @@ -223,11 +225,19 @@ impl PyString { return Ok(PyString::new(py, static_string)); }; - let mut writer = PyUnicodeWriter::new(py)?; - writer - .write_fmt(args) - .map_err(|_| writer.take_error().expect("expected error"))?; - writer.into_py_string(py) + #[cfg(not(Py_LIMITED_API))] + { + let mut writer = PyUnicodeWriter::new(py)?; + writer + .write_fmt(args) + .map_err(|_| writer.take_error().expect("expected error"))?; + writer.into_py_string(py) + } + + #[cfg(Py_LIMITED_API)] + { + Ok(PyString::new(py, &format!("{args}"))) + } } } From a9980943f3b0ed572e0443a08fcc24b9457691c3 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 19 Jun 2025 15:47:54 +0200 Subject: [PATCH 4/7] fix clippy --- src/fmt.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/fmt.rs b/src/fmt.rs index 0c078a09092..c293d566a7c 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -118,6 +118,7 @@ mod tests { use crate::{IntoPyObject, Python}; #[test] + #[allow(clippy::write_literal)] #[cfg(not(Py_LIMITED_API))] fn unicode_writer_test() { use std::fmt::Write; @@ -140,10 +141,10 @@ mod tests { #[test] fn test_complex_format() { Python::with_gil(|py| { - let complex_value = (42, "foo", 3.14).into_pyobject(py).unwrap(); + let complex_value = (42, "foo", [0; 0]).into_pyobject(py).unwrap(); let py_string = py_format!(py, "This is some complex value: {complex_value}").unwrap(); let actual = py_string.to_cow().unwrap(); - let expected = "This is some complex value: (42, 'foo', 3.14)"; + let expected = "This is some complex value: (42, 'foo', [])"; assert_eq!(actual, expected); }); } From 657ac7fbd2b24d49edcc1f7589e4d47a3ee6e2f7 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 19 Jun 2025 17:01:16 +0200 Subject: [PATCH 5/7] disable compat functions on `abi3` --- pyo3-ffi/src/compat/py_3_14.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyo3-ffi/src/compat/py_3_14.rs b/pyo3-ffi/src/compat/py_3_14.rs index f2cf0d0c143..a1ba2e7bf99 100644 --- a/pyo3-ffi/src/compat/py_3_14.rs +++ b/pyo3-ffi/src/compat/py_3_14.rs @@ -25,10 +25,10 @@ compat_function!( } ); +#[cfg(not(Py_LIMITED_API))] compat_function!( originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); - #[cfg(not(Py_LIMITED_API))] pub unsafe fn PyUnicodeWriter_Create(length: crate::Py_ssize_t) -> *mut crate::PyUnicodeWriter { if length < 0 { crate::PyErr_SetString( @@ -50,10 +50,10 @@ compat_function!( } ); +#[cfg(not(Py_LIMITED_API))] compat_function!( originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); - #[cfg(not(Py_LIMITED_API))] pub unsafe fn PyUnicodeWriter_Finish(writer: *mut crate::PyUnicodeWriter) -> *mut crate::PyObject { let str = crate::_PyUnicodeWriter_Finish(writer.cast()); crate::PyMem_Free(writer.cast()); @@ -61,20 +61,20 @@ compat_function!( } ); +#[cfg(not(Py_LIMITED_API))] compat_function!( originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); - #[cfg(not(Py_LIMITED_API))] pub unsafe fn PyUnicodeWriter_Discard(writer: *mut crate::PyUnicodeWriter) -> () { crate::_PyUnicodeWriter_Dealloc(writer.cast()); crate::PyMem_Free(writer.cast()) } ); +#[cfg(not(Py_LIMITED_API))] compat_function!( originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); - #[cfg(not(Py_LIMITED_API))] pub unsafe fn PyUnicodeWriter_WriteChar(writer: *mut crate::PyUnicodeWriter, ch: crate::Py_UCS4) -> std::os::raw::c_int { if ch > 0x10ffff { crate::PyErr_SetString( @@ -88,10 +88,10 @@ compat_function!( } ); +#[cfg(not(Py_LIMITED_API))] compat_function!( originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); - #[cfg(not(Py_LIMITED_API))] pub unsafe fn PyUnicodeWriter_WriteUTF8(writer: *mut crate::PyUnicodeWriter,str: *const std::os::raw::c_char, size: crate::Py_ssize_t) -> std::os::raw::c_int { let size = if size < 0 { libc::strlen(str) as isize From 203dec2f94c60683049225423f9a4f9c11be5209 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Thu, 19 Jun 2025 17:38:26 +0200 Subject: [PATCH 6/7] Disable on pypy --- pyo3-ffi/src/compat/py_3_14.rs | 10 +++++----- src/fmt.rs | 14 +++++++------- src/types/string.rs | 8 ++++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/pyo3-ffi/src/compat/py_3_14.rs b/pyo3-ffi/src/compat/py_3_14.rs index a1ba2e7bf99..14a59c979bd 100644 --- a/pyo3-ffi/src/compat/py_3_14.rs +++ b/pyo3-ffi/src/compat/py_3_14.rs @@ -25,7 +25,7 @@ compat_function!( } ); -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, PyPy)))] compat_function!( originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); @@ -50,7 +50,7 @@ compat_function!( } ); -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, PyPy)))] compat_function!( originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); @@ -61,7 +61,7 @@ compat_function!( } ); -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, PyPy)))] compat_function!( originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); @@ -71,7 +71,7 @@ compat_function!( } ); -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, PyPy)))] compat_function!( originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); @@ -88,7 +88,7 @@ compat_function!( } ); -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, PyPy)))] compat_function!( originally_defined_for(all(Py_3_14, not(Py_LIMITED_API))); diff --git a/src/fmt.rs b/src/fmt.rs index c293d566a7c..9a33d3016be 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -2,7 +2,7 @@ //! constructing Python strings using Rust's `fmt::Write` trait. //! It allows for incremental string construction, without the need for repeated allocations, and //! is particularly useful for building strings in a performance-sensitive context. -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, PyPy)))] use { crate::ffi::compat::{ PyUnicodeWriter_Create, PyUnicodeWriter_Discard, PyUnicodeWriter_Finish, @@ -24,14 +24,14 @@ macro_rules! py_format { } } -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, PyPy)))] /// The `PyUnicodeWriter` is a utility for efficiently constructing Python strings pub struct PyUnicodeWriter { writer: NonNull, last_error: Option, } -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, PyPy)))] impl PyUnicodeWriter { /// Creates a new `PyUnicodeWriter`. pub fn new(py: Python<'_>) -> PyResult { @@ -76,7 +76,7 @@ impl PyUnicodeWriter { } } -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, PyPy)))] impl fmt::Write for PyUnicodeWriter { fn write_str(&mut self, s: &str) -> fmt::Result { let result = unsafe { @@ -101,7 +101,7 @@ impl fmt::Write for PyUnicodeWriter { } } -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, PyPy)))] impl Drop for PyUnicodeWriter { fn drop(&mut self) { unsafe { @@ -112,14 +112,14 @@ impl Drop for PyUnicodeWriter { #[cfg(test)] mod tests { - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, PyPy)))] use super::*; use crate::types::PyStringMethods; use crate::{IntoPyObject, Python}; #[test] #[allow(clippy::write_literal)] - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, PyPy)))] fn unicode_writer_test() { use std::fmt::Write; Python::with_gil(|py| { diff --git a/src/types/string.rs b/src/types/string.rs index b0bfa94894c..7d7d2701de2 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -1,7 +1,7 @@ #[cfg(not(Py_LIMITED_API))] use crate::exceptions::PyUnicodeDecodeError; use crate::ffi_ptr_ext::FfiPtrExt; -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, PyPy)))] use crate::fmt::PyUnicodeWriter; use crate::instance::Borrowed; use crate::py_result_ext::PyResultExt; @@ -11,7 +11,7 @@ use crate::types::PyBytes; use crate::{ffi, Bound, Py, PyAny, PyResult, Python}; use std::borrow::Cow; use std::ffi::CString; -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, PyPy)))] use std::fmt::Write as _; use std::{fmt, str}; @@ -225,7 +225,7 @@ impl PyString { return Ok(PyString::new(py, static_string)); }; - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, PyPy)))] { let mut writer = PyUnicodeWriter::new(py)?; writer @@ -234,7 +234,7 @@ impl PyString { writer.into_py_string(py) } - #[cfg(Py_LIMITED_API)] + #[cfg(any(Py_LIMITED_API, PyPy))] { Ok(PyString::new(py, &format!("{args}"))) } From 5644e0cad853bb9c84ea2259f10d2ea00a7cbf43 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Sat, 21 Jun 2025 10:44:00 +0200 Subject: [PATCH 7/7] Consume current error in `into_py_string` --- src/fmt.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/fmt.rs b/src/fmt.rs index 9a33d3016be..f8c9a108e27 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -50,14 +50,18 @@ impl PyUnicodeWriter { } /// Consumes the `PyUnicodeWriter` and returns a `Bound` containing the constructed string. - pub fn into_py_string(self, py: Python<'_>) -> PyResult> { - let writer_ptr = self.as_ptr(); - mem::forget(self); - Ok(unsafe { - PyUnicodeWriter_Finish(writer_ptr) - .assume_owned_or_err(py)? - .downcast_into_unchecked() - }) + pub fn into_py_string(mut self, py: Python<'_>) -> PyResult> { + if let Some(error) = self.take_error() { + Err(error) + } else { + let writer_ptr = self.as_ptr(); + mem::forget(self); + Ok(unsafe { + PyUnicodeWriter_Finish(writer_ptr) + .assume_owned_or_err(py)? + .downcast_into_unchecked() + }) + } } /// When fmt::Write returned an error, this function can be used to retrieve the last error that occurred.