From cda01714aee4a1818233ec604c510abe494cc5dc Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 14 May 2025 09:18:24 -0400 Subject: [PATCH 01/16] docs: fixup typo on object.md (#5143) --- guide/src/class/object.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/guide/src/class/object.md b/guide/src/class/object.md index 3b9f1aafa40..a3a136fe015 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -77,8 +77,8 @@ To automatically generate the `__str__` implementation using a `Display` trait i # use pyo3::prelude::*; # # #[allow(dead_code)] -# #[pyclass(str)] -# struct Coordinate { +#[pyclass(str)] +struct Coordinate { x: i32, y: i32, z: i32, @@ -104,8 +104,8 @@ For convenience, a shorthand format string can be passed to `str` as `str="
Date: Wed, 14 May 2025 10:42:06 +0200 Subject: [PATCH 02/16] Implements basic method introspection (#5087) * Implements basic method introspection * Code review feedback * Fixes CI --- pyo3-introspection/src/introspection.rs | 189 ++++++++++++++++------- pyo3-introspection/src/model.rs | 3 + pyo3-introspection/src/stubs.rs | 48 +++++- pyo3-introspection/tests/test.rs | 5 +- pyo3-macros-backend/src/introspection.rs | 77 +++++++-- pyo3-macros-backend/src/pyfunction.rs | 10 +- pyo3-macros-backend/src/pyimpl.rs | 71 ++++++++- pyo3-macros-backend/src/pymethod.rs | 14 +- pytests/src/pyclasses.rs | 42 ++++- pytests/stubs/pyclasses.pyi | 34 +++- pytests/tests/test_pyclasses.py | 29 ++++ 11 files changed, 421 insertions(+), 101 deletions(-) diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index e4f49d5e0e3..665109d0a66 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -7,6 +7,7 @@ use goblin::mach::{Mach, MachO, SingleArch}; use goblin::pe::PE; use goblin::Object; use serde::Deserialize; +use std::cmp::Ordering; use std::collections::HashMap; use std::fs; use std::path::Path; @@ -21,19 +22,23 @@ pub fn introspect_cdylib(library_path: impl AsRef, main_module_name: &str) /// Parses the introspection chunks found in the binary fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result { - let chunks_by_id = chunks - .iter() - .map(|c| { - ( - match c { - Chunk::Module { id, .. } => id, - Chunk::Class { id, .. } => id, - Chunk::Function { id, .. } => id, - }, - c, - ) - }) - .collect::>(); + let mut chunks_by_id = HashMap::<&str, &Chunk>::new(); + let mut chunks_by_parent = HashMap::<&str, Vec<&Chunk>>::new(); + for chunk in chunks { + if let Some(id) = match chunk { + Chunk::Module { id, .. } => Some(id), + Chunk::Class { id, .. } => Some(id), + Chunk::Function { id, .. } => id.as_ref(), + } { + chunks_by_id.insert(id, chunk); + } + if let Some(parent) = match chunk { + Chunk::Module { .. } | Chunk::Class { .. } => None, + Chunk::Function { parent, .. } => parent.as_ref(), + } { + chunks_by_parent.entry(parent).or_default().push(chunk); + } + } // We look for the root chunk for chunk in chunks { if let Chunk::Module { @@ -43,7 +48,7 @@ fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result { } = chunk { if name == main_module_name { - return convert_module(name, members, &chunks_by_id); + return convert_module(name, members, &chunks_by_id, &chunks_by_parent); } } } @@ -53,61 +58,126 @@ fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result { fn convert_module( name: &str, members: &[String], - chunks_by_id: &HashMap<&String, &Chunk>, + chunks_by_id: &HashMap<&str, &Chunk>, + chunks_by_parent: &HashMap<&str, Vec<&Chunk>>, ) -> Result { + let (modules, classes, functions) = convert_members( + &members + .iter() + .filter_map(|id| chunks_by_id.get(id.as_str()).copied()) + .collect::>(), + chunks_by_id, + chunks_by_parent, + )?; + Ok(Module { + name: name.into(), + modules, + classes, + functions, + }) +} + +/// Convert a list of members of a module or a class +fn convert_members( + chunks: &[&Chunk], + chunks_by_id: &HashMap<&str, &Chunk>, + chunks_by_parent: &HashMap<&str, Vec<&Chunk>>, +) -> Result<(Vec, Vec, Vec)> { let mut modules = Vec::new(); let mut classes = Vec::new(); let mut functions = Vec::new(); - for member in members { - if let Some(chunk) = chunks_by_id.get(member) { - match chunk { - Chunk::Module { + for chunk in chunks { + match chunk { + Chunk::Module { + name, + members, + id: _, + } => { + modules.push(convert_module( name, members, - id: _, - } => { - modules.push(convert_module(name, members, chunks_by_id)?); - } - Chunk::Class { name, id: _ } => classes.push(Class { name: name.into() }), - Chunk::Function { - name, - id: _, - arguments, - } => functions.push(Function { - name: name.into(), - arguments: Arguments { - positional_only_arguments: arguments - .posonlyargs - .iter() - .map(convert_argument) - .collect(), - arguments: arguments.args.iter().map(convert_argument).collect(), - vararg: arguments - .vararg - .as_ref() - .map(convert_variable_length_argument), - keyword_only_arguments: arguments - .kwonlyargs - .iter() - .map(convert_argument) - .collect(), - kwarg: arguments - .kwarg - .as_ref() - .map(convert_variable_length_argument), - }, - }), + chunks_by_id, + chunks_by_parent, + )?); } + Chunk::Class { name, id } => { + classes.push(convert_class(id, name, chunks_by_id, chunks_by_parent)?) + } + Chunk::Function { + name, + id: _, + arguments, + parent: _, + decorators, + } => functions.push(convert_function(name, arguments, decorators)), } } - Ok(Module { + Ok((modules, classes, functions)) +} + +fn convert_class( + id: &str, + name: &str, + chunks_by_id: &HashMap<&str, &Chunk>, + chunks_by_parent: &HashMap<&str, Vec<&Chunk>>, +) -> Result { + let (nested_modules, nested_classes, mut methods) = convert_members( + chunks_by_parent + .get(&id) + .map(Vec::as_slice) + .unwrap_or_default(), + chunks_by_id, + chunks_by_parent, + )?; + ensure!( + nested_modules.is_empty(), + "Classes cannot contain nested modules" + ); + ensure!( + nested_classes.is_empty(), + "Nested classes are not supported yet" + ); + // We sort methods to get a stable output + methods.sort_by(|l, r| match l.name.cmp(&r.name) { + Ordering::Equal => { + // We put the getter before the setter + if l.decorators.iter().any(|d| d == "property") { + Ordering::Less + } else if r.decorators.iter().any(|d| d == "property") { + Ordering::Greater + } else { + // We pick an ordering based on decorators + l.decorators.cmp(&r.decorators) + } + } + o => o, + }); + Ok(Class { name: name.into(), - modules, - classes, - functions, + methods, }) } +fn convert_function(name: &str, arguments: &ChunkArguments, decorators: &[String]) -> Function { + Function { + name: name.into(), + decorators: decorators.to_vec(), + arguments: Arguments { + positional_only_arguments: arguments.posonlyargs.iter().map(convert_argument).collect(), + arguments: arguments.args.iter().map(convert_argument).collect(), + vararg: arguments + .vararg + .as_ref() + .map(convert_variable_length_argument), + keyword_only_arguments: arguments.kwonlyargs.iter().map(convert_argument).collect(), + kwarg: arguments + .kwarg + .as_ref() + .map(convert_variable_length_argument), + }, + } +} + fn convert_argument(arg: &ChunkArgument) -> Argument { Argument { name: arg.name.clone(), @@ -290,9 +360,14 @@ enum Chunk { name: String, }, Function { - id: String, + #[serde(default)] + id: Option, name: String, arguments: ChunkArguments, + #[serde(default)] + parent: Option, + #[serde(default)] + decorators: Vec, }, } diff --git a/pyo3-introspection/src/model.rs b/pyo3-introspection/src/model.rs index 7705a0006a4..021475b9392 100644 --- a/pyo3-introspection/src/model.rs +++ b/pyo3-introspection/src/model.rs @@ -9,11 +9,14 @@ pub struct Module { #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub struct Class { pub name: String, + pub methods: Vec, } #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub struct Function { pub name: String, + /// decorator like 'property' or 'staticmethod' + pub decorators: Vec, pub arguments: Arguments, } diff --git a/pyo3-introspection/src/stubs.rs b/pyo3-introspection/src/stubs.rs index 2312d7d37ac..cc0a11ebd38 100644 --- a/pyo3-introspection/src/stubs.rs +++ b/pyo3-introspection/src/stubs.rs @@ -39,12 +39,39 @@ fn module_stubs(module: &Module) -> String { for function in &module.functions { elements.push(function_stubs(function)); } - elements.push(String::new()); // last line jump - elements.join("\n") + + // We insert two line jumps (i.e. empty strings) only above and below multiple line elements (classes with methods, functions with decorators) + let mut output = String::new(); + for element in elements { + let is_multiline = element.contains('\n'); + if is_multiline && !output.is_empty() && !output.ends_with("\n\n") { + output.push('\n'); + } + output.push_str(&element); + output.push('\n'); + if is_multiline { + output.push('\n'); + } + } + // We remove a line jump at the end if they are two + if output.ends_with("\n\n") { + output.pop(); + } + output } fn class_stubs(class: &Class) -> String { - format!("class {}: ...", class.name) + let mut buffer = format!("class {}:", class.name); + if class.methods.is_empty() { + buffer.push_str(" ..."); + return buffer; + } + for method in &class.methods { + // We do the indentation + buffer.push_str("\n "); + buffer.push_str(&function_stubs(method).replace('\n', "\n ")); + } + buffer } fn function_stubs(function: &Function) -> String { @@ -70,7 +97,18 @@ fn function_stubs(function: &Function) -> String { if let Some(argument) = &function.arguments.kwarg { parameters.push(format!("**{}", variable_length_argument_stub(argument))); } - format!("def {}({}): ...", function.name, parameters.join(", ")) + let output = format!("def {}({}): ...", function.name, parameters.join(", ")); + if function.decorators.is_empty() { + return output; + } + let mut buffer = String::new(); + for decorator in &function.decorators { + buffer.push('@'); + buffer.push_str(decorator); + buffer.push('\n'); + } + buffer.push_str(&output); + buffer } fn argument_stub(argument: &Argument) -> String { @@ -95,6 +133,7 @@ mod tests { fn function_stubs_with_variable_length() { let function = Function { name: "func".into(), + decorators: Vec::new(), arguments: Arguments { positional_only_arguments: vec![Argument { name: "posonly".into(), @@ -126,6 +165,7 @@ mod tests { fn function_stubs_without_variable_length() { let function = Function { name: "afunc".into(), + decorators: Vec::new(), arguments: Arguments { positional_only_arguments: vec![Argument { name: "posonly".into(), diff --git a/pyo3-introspection/tests/test.rs b/pyo3-introspection/tests/test.rs index 37070a53a13..302efd1ea47 100644 --- a/pyo3-introspection/tests/test.rs +++ b/pyo3-introspection/tests/test.rs @@ -42,9 +42,10 @@ fn pytests_stubs() -> Result<()> { file_name.display() ) }); + // We normalize line jumps for compatibility with Windows assert_eq!( - &expected_file_content.replace('\r', ""), // Windows compatibility - actual_file_content, + expected_file_content.replace('\r', ""), + actual_file_content.replace('\r', ""), "The content of file {} is different", file_name.display() ) diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index 4888417cb08..303e952b814 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -19,7 +19,7 @@ use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::mem::take; use std::sync::atomic::{AtomicUsize, Ordering}; -use syn::{Attribute, Ident}; +use syn::{Attribute, Ident, Type, TypePath}; static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0); @@ -42,7 +42,9 @@ pub fn module_introspection_code<'a>( .zip(members_cfg_attrs) .filter_map(|(member, attributes)| { if attributes.is_empty() { - Some(IntrospectionNode::IntrospectionId(Some(member))) + Some(IntrospectionNode::IntrospectionId(Some(ident_to_type( + member, + )))) } else { None // TODO: properly interpret cfg attributes } @@ -64,7 +66,10 @@ pub fn class_introspection_code( IntrospectionNode::Map( [ ("type", IntrospectionNode::String("class".into())), - ("id", IntrospectionNode::IntrospectionId(Some(ident))), + ( + "id", + IntrospectionNode::IntrospectionId(Some(ident_to_type(ident))), + ), ("name", IntrospectionNode::String(name.into())), ] .into(), @@ -74,23 +79,47 @@ pub fn class_introspection_code( pub fn function_introspection_code( pyo3_crate_path: &PyO3CratePath, - ident: &Ident, + ident: Option<&Ident>, name: &str, signature: &FunctionSignature<'_>, + first_argument: Option<&'static str>, + decorators: impl IntoIterator, + parent: Option<&Type>, ) -> TokenStream { - IntrospectionNode::Map( - [ - ("type", IntrospectionNode::String("function".into())), - ("id", IntrospectionNode::IntrospectionId(Some(ident))), - ("name", IntrospectionNode::String(name.into())), - ("arguments", arguments_introspection_data(signature)), - ] - .into(), - ) - .emit(pyo3_crate_path) + let mut desc = HashMap::from([ + ("type", IntrospectionNode::String("function".into())), + ("name", IntrospectionNode::String(name.into())), + ( + "arguments", + arguments_introspection_data(signature, first_argument), + ), + ]); + if let Some(ident) = ident { + desc.insert( + "id", + IntrospectionNode::IntrospectionId(Some(ident_to_type(ident))), + ); + } + let decorators = decorators + .into_iter() + .map(|d| IntrospectionNode::String(d.into())) + .collect::>(); + if !decorators.is_empty() { + desc.insert("decorators", IntrospectionNode::List(decorators)); + } + if let Some(parent) = parent { + desc.insert( + "parent", + IntrospectionNode::IntrospectionId(Some(Cow::Borrowed(parent))), + ); + } + IntrospectionNode::Map(desc).emit(pyo3_crate_path) } -fn arguments_introspection_data<'a>(signature: &'a FunctionSignature<'a>) -> IntrospectionNode<'a> { +fn arguments_introspection_data<'a>( + signature: &'a FunctionSignature<'a>, + first_argument: Option<&'a str>, +) -> IntrospectionNode<'a> { let mut argument_desc = signature.arguments.iter().filter_map(|arg| { if let FnArg::Regular(arg) = arg { Some(arg) @@ -105,6 +134,12 @@ fn arguments_introspection_data<'a>(signature: &'a FunctionSignature<'a>) -> Int let mut kwonlyargs = Vec::new(); let mut kwarg = None; + if let Some(first_argument) = first_argument { + posonlyargs.push(IntrospectionNode::Map( + [("name", IntrospectionNode::String(first_argument.into()))].into(), + )); + } + for (i, param) in signature .python_signature .positional_parameters @@ -184,7 +219,7 @@ fn argument_introspection_data<'a>( enum IntrospectionNode<'a> { String(Cow<'a, str>), - IntrospectionId(Option<&'a Ident>), + IntrospectionId(Option>), Map(HashMap<&'static str, IntrospectionNode<'a>>), List(Vec>), } @@ -333,3 +368,13 @@ fn unique_element_id() -> u64 { .hash(&mut hasher); // If there are multiple elements in the same call site hasher.finish() } + +fn ident_to_type(ident: &Ident) -> Cow<'static, Type> { + Cow::Owned( + TypePath { + path: ident.clone().into(), + qself: None, + } + .into(), + ) +} diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 301819f42dd..e512ca1cabc 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -230,7 +230,15 @@ pub fn impl_wrap_pyfunction( let name = &func.sig.ident; #[cfg(feature = "experimental-inspect")] - let introspection = function_introspection_code(pyo3_path, name, &name.to_string(), &signature); + let introspection = function_introspection_code( + pyo3_path, + Some(name), + &name.to_string(), + &signature, + None, + [] as [String; 0], + None, + ); #[cfg(not(feature = "experimental-inspect"))] let introspection = quote! {}; #[cfg(feature = "experimental-inspect")] diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 72f06721ec4..90b0d961cd8 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -1,14 +1,19 @@ use std::collections::HashSet; +#[cfg(feature = "experimental-inspect")] +use crate::introspection::function_introspection_code; +#[cfg(feature = "experimental-inspect")] +use crate::method::{FnSpec, FnType}; use crate::utils::{has_attribute, has_attribute_with_namespace, Ctx, PyO3CratePath}; use crate::{ attributes::{take_pyo3_options, CrateAttribute}, konst::{ConstAttributes, ConstSpec}, pyfunction::PyFunctionOptions, - pymethod::{self, is_proto_method, MethodAndMethodDef, MethodAndSlotDef}, + pymethod::{ + self, is_proto_method, GeneratedPyMethod, MethodAndMethodDef, MethodAndSlotDef, PyMethod, + }, }; use proc_macro2::TokenStream; -use pymethod::GeneratedPyMethod; use quote::{format_ident, quote}; use syn::ImplItemFn; use syn::{ @@ -110,7 +115,7 @@ pub fn impl_methods( methods_type: PyClassMethodsType, options: PyImplOptions, ) -> syn::Result { - let mut trait_impls = Vec::new(); + let mut extra_fragments = Vec::new(); let mut proto_impls = Vec::new(); let mut methods = Vec::new(); let mut associated_methods = Vec::new(); @@ -125,9 +130,10 @@ pub fn impl_methods( fun_options.krate = fun_options.krate.or_else(|| options.krate.clone()); check_pyfunction(&ctx.pyo3_path, meth)?; - - match pymethod::gen_py_method(ty, &mut meth.sig, &mut meth.attrs, fun_options, ctx)? - { + let method = PyMethod::parse(&mut meth.sig, &mut meth.attrs, fun_options)?; + #[cfg(feature = "experimental-inspect")] + extra_fragments.push(method_introspection_code(&method.spec, ty, ctx)); + match pymethod::gen_py_method(ty, method, &meth.attrs, ctx)? { GeneratedPyMethod::Method(MethodAndMethodDef { associated_method, method_def, @@ -139,7 +145,7 @@ pub fn impl_methods( GeneratedPyMethod::SlotTraitImpl(method_name, token_stream) => { implemented_proto_fragments.insert(method_name); let attrs = get_cfg_attributes(&meth.attrs); - trait_impls.push(quote!(#(#attrs)* #token_stream)); + extra_fragments.push(quote!(#(#attrs)* #token_stream)); } GeneratedPyMethod::Proto(MethodAndSlotDef { associated_method, @@ -193,7 +199,7 @@ pub fn impl_methods( }; Ok(quote! { - #(#trait_impls)* + #(#extra_fragments)* #items @@ -336,3 +342,52 @@ pub(crate) fn get_cfg_attributes(attrs: &[syn::Attribute]) -> Vec<&syn::Attribut .filter(|attr| attr.path().is_ident("cfg")) .collect() } + +#[cfg(feature = "experimental-inspect")] +fn method_introspection_code(spec: &FnSpec<'_>, parent: &syn::Type, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path, .. } = ctx; + + // We introduce self/cls argument and setup decorators + let name = spec.python_name.to_string(); + let mut first_argument = None; + let mut decorators = Vec::new(); + match &spec.tp { + FnType::Getter(_) => { + first_argument = Some("self"); + decorators.push("property".into()); + } + FnType::Setter(_) => { + first_argument = Some("self"); + decorators.push(format!("{name}.setter")); + } + FnType::Fn(_) => { + first_argument = Some("self"); + } + FnType::FnNew | FnType::FnNewClass(_) => { + first_argument = Some("cls"); + } + FnType::FnClass(_) => { + first_argument = Some("cls"); + decorators.push("classmethod".into()); + } + FnType::FnStatic => { + decorators.push("staticmethod".into()); + } + FnType::FnModule(_) => (), // TODO: not sure this can happen + FnType::ClassAttribute => { + first_argument = Some("cls"); + // TODO: this combination only works with Python 3.9-3.11 https://docs.python.org/3.11/library/functions.html#classmethod + decorators.push("classmethod".into()); + decorators.push("property".into()); + } + } + function_introspection_code( + pyo3_path, + None, + &name, + &spec.signature, + first_argument, + decorators, + Some(parent), + ) +} diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 15cce6f365f..434a96e9dde 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -40,7 +40,7 @@ pub enum GeneratedPyMethod { pub struct PyMethod<'a> { kind: PyMethodKind, method_name: String, - spec: FnSpec<'a>, + pub spec: FnSpec<'a>, } enum PyMethodKind { @@ -160,11 +160,13 @@ enum PyMethodProtoKind { } impl<'a> PyMethod<'a> { - fn parse( + pub fn parse( sig: &'a mut syn::Signature, meth_attrs: &mut Vec, options: PyFunctionOptions, ) -> Result { + check_generic(sig)?; + ensure_function_options_valid(&options)?; let spec = FnSpec::parse(sig, meth_attrs, options)?; let method_name = spec.python_name.to_string(); @@ -187,14 +189,10 @@ pub fn is_proto_method(name: &str) -> bool { pub fn gen_py_method( cls: &syn::Type, - sig: &mut syn::Signature, - meth_attrs: &mut Vec, - options: PyFunctionOptions, + method: PyMethod<'_>, + meth_attrs: &[syn::Attribute], ctx: &Ctx, ) -> Result { - check_generic(sig)?; - ensure_function_options_valid(&options)?; - let method = PyMethod::parse(sig, meth_attrs, options)?; let spec = &method.spec; let Ctx { pyo3_path, .. } = ctx; diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index 4c3398b6627..4e681b2c941 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -103,6 +103,45 @@ impl ClassWithDict { } } +#[pyclass] +struct ClassWithDecorators { + attr: usize, +} + +#[pymethods] +impl ClassWithDecorators { + #[new] + #[classmethod] + fn new(_cls: Bound<'_, PyType>) -> Self { + Self { attr: 0 } + } + + #[getter] + fn get_attr(&self) -> usize { + self.attr + } + + #[setter] + fn set_attr(&mut self, value: usize) { + self.attr = value; + } + + #[classmethod] + fn cls_method(_cls: &Bound<'_, PyType>) -> usize { + 1 + } + + #[staticmethod] + fn static_method() -> usize { + 2 + } + + #[classattr] + fn cls_attribute() -> usize { + 3 + } +} + #[pymodule(gil_used = false)] pub mod pyclasses { #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] @@ -110,6 +149,7 @@ pub mod pyclasses { use super::ClassWithDict; #[pymodule_export] use super::{ - AssertingBaseClass, ClassWithoutConstructor, EmptyClass, PyClassIter, PyClassThreadIter, + AssertingBaseClass, ClassWithDecorators, ClassWithoutConstructor, EmptyClass, PyClassIter, + PyClassThreadIter, }; } diff --git a/pytests/stubs/pyclasses.pyi b/pytests/stubs/pyclasses.pyi index 2a36e0b4540..13b52f1c4e2 100644 --- a/pytests/stubs/pyclasses.pyi +++ b/pytests/stubs/pyclasses.pyi @@ -1,5 +1,31 @@ -class AssertingBaseClass: ... +class AssertingBaseClass: + def __new__(cls, /, expected_type): ... + +class ClassWithDecorators: + def __new__(cls, /): ... + @property + def attr(self, /): ... + @attr.setter + def attr(self, /, value): ... + @classmethod + @property + def cls_attribute(cls, /): ... + @classmethod + def cls_method(cls, /): ... + @staticmethod + def static_method(): ... + class ClassWithoutConstructor: ... -class EmptyClass: ... -class PyClassIter: ... -class PyClassThreadIter: ... + +class EmptyClass: + def __len__(self, /): ... + def __new__(cls, /): ... + def method(self, /): ... + +class PyClassIter: + def __new__(cls, /): ... + def __next__(self, /): ... + +class PyClassThreadIter: + def __new__(cls, /): ... + def __next__(self, /): ... diff --git a/pytests/tests/test_pyclasses.py b/pytests/tests/test_pyclasses.py index 9f611b634b6..a641c9770a4 100644 --- a/pytests/tests/test_pyclasses.py +++ b/pytests/tests/test_pyclasses.py @@ -121,3 +121,32 @@ def test_dict(): d.foo = 42 assert d.__dict__ == {"foo": 42} + + +def test_getter(benchmark): + obj = pyclasses.ClassWithDecorators() + benchmark(lambda: obj.attr) + + +def test_setter(benchmark): + obj = pyclasses.ClassWithDecorators() + + def set_attr(): + obj.attr = 42 + + benchmark(set_attr) + + +def test_class_attribute(benchmark): + cls = pyclasses.ClassWithDecorators + benchmark(lambda: cls.cls_attribute) + + +def test_class_method(benchmark): + cls = pyclasses.ClassWithDecorators + benchmark(lambda: cls.cls_method()) + + +def test_static_method(benchmark): + cls = pyclasses.ClassWithDecorators + benchmark(lambda: cls.static_method()) From 13ae76b28ef6df6d7cbc9fb8280f81fce9abb043 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 17 May 2025 03:11:58 +0200 Subject: [PATCH 03/16] ci: updates for Rust 1.87 (#5147) --- tests/ui/invalid_proto_pymethods.stderr | 22 +++---- tests/ui/pyclass_send.stderr | 84 ++++++++++++------------- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/tests/ui/invalid_proto_pymethods.stderr b/tests/ui/invalid_proto_pymethods.stderr index 82c99c2ddc3..1f564d18ea0 100644 --- a/tests/ui/invalid_proto_pymethods.stderr +++ b/tests/ui/invalid_proto_pymethods.stderr @@ -22,6 +22,17 @@ error: `text_signature` cannot be used with magic method `__bool__` 46 | #[pyo3(name = "__bool__", text_signature = "")] | ^^^^^^^^^^^^^^ +error[E0592]: duplicate definitions with name `__pymethod___richcmp____` + --> tests/ui/invalid_proto_pymethods.rs:55:1 + | +55 | #[pymethods] + | ^^^^^^^^^^^^ + | | + | duplicate definitions for `__pymethod___richcmp____` + | other definition for `__pymethod___richcmp____` + | + = note: this error originates in the macro `::pyo3::impl_::pyclass::generate_pyclass_richcompare_slot` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + error[E0034]: multiple applicable items in scope --> tests/ui/invalid_proto_pymethods.rs:55:1 | @@ -40,17 +51,6 @@ note: candidate #2 is defined in an impl for the type `EqAndRichcmp` | ^^^^^^^^^^^^ = note: this error originates in the macro `::pyo3::impl_::pyclass::generate_pyclass_richcompare_slot` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0592]: duplicate definitions with name `__pymethod___richcmp____` - --> tests/ui/invalid_proto_pymethods.rs:55:1 - | -55 | #[pymethods] - | ^^^^^^^^^^^^ - | | - | duplicate definitions for `__pymethod___richcmp____` - | other definition for `__pymethod___richcmp____` - | - = note: this error originates in the macro `::pyo3::impl_::pyclass::generate_pyclass_richcompare_slot` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) - error[E0034]: multiple applicable items in scope --> tests/ui/invalid_proto_pymethods.rs:55:1 | diff --git a/tests/ui/pyclass_send.stderr b/tests/ui/pyclass_send.stderr index 1623a5b2183..334c36daa13 100644 --- a/tests/ui/pyclass_send.stderr +++ b/tests/ui/pyclass_send.stderr @@ -1,24 +1,3 @@ -error[E0277]: `*mut c_void` cannot be shared between threads safely - --> tests/ui/pyclass_send.rs:5:8 - | -5 | struct NotSyncNotSend(*mut c_void); - | ^^^^^^^^^^^^^^ `*mut c_void` cannot be shared between threads safely - | - = help: within `NotSyncNotSend`, the trait `Sync` is not implemented for `*mut c_void` -note: required because it appears within the type `NotSyncNotSend` - --> tests/ui/pyclass_send.rs:5:8 - | -5 | struct NotSyncNotSend(*mut c_void); - | ^^^^^^^^^^^^^^ -note: required by a bound in `assert_pyclass_sync` - --> src/impl_/pyclass/assertions.rs - | - | pub const fn assert_pyclass_sync() - | ------------------- required by a bound in this function - | where - | T: PyClassSync + Sync, - | ^^^^ required by this bound in `assert_pyclass_sync` - error[E0277]: `*mut c_void` cannot be sent between threads safely --> tests/ui/pyclass_send.rs:4:1 | @@ -40,27 +19,6 @@ note: required by a bound in `PyClassImpl::ThreadChecker` | ^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyClassImpl::ThreadChecker` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0277]: `*mut c_void` cannot be shared between threads safely - --> tests/ui/pyclass_send.rs:8:8 - | -8 | struct SendNotSync(*mut c_void); - | ^^^^^^^^^^^ `*mut c_void` cannot be shared between threads safely - | - = help: within `SendNotSync`, the trait `Sync` is not implemented for `*mut c_void` -note: required because it appears within the type `SendNotSync` - --> tests/ui/pyclass_send.rs:8:8 - | -8 | struct SendNotSync(*mut c_void); - | ^^^^^^^^^^^ -note: required by a bound in `assert_pyclass_sync` - --> src/impl_/pyclass/assertions.rs - | - | pub const fn assert_pyclass_sync() - | ------------------- required by a bound in this function - | where - | T: PyClassSync + Sync, - | ^^^^ required by this bound in `assert_pyclass_sync` - error[E0277]: `*mut c_void` cannot be sent between threads safely --> tests/ui/pyclass_send.rs:11:1 | @@ -119,3 +77,45 @@ note: required by a bound in `SendablePyClass` | pub struct SendablePyClass(PhantomData); | ^^^^ required by this bound in `SendablePyClass` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: `*mut c_void` cannot be shared between threads safely + --> tests/ui/pyclass_send.rs:5:8 + | +5 | struct NotSyncNotSend(*mut c_void); + | ^^^^^^^^^^^^^^ `*mut c_void` cannot be shared between threads safely + | + = help: within `NotSyncNotSend`, the trait `Sync` is not implemented for `*mut c_void` +note: required because it appears within the type `NotSyncNotSend` + --> tests/ui/pyclass_send.rs:5:8 + | +5 | struct NotSyncNotSend(*mut c_void); + | ^^^^^^^^^^^^^^ +note: required by a bound in `assert_pyclass_sync` + --> src/impl_/pyclass/assertions.rs + | + | pub const fn assert_pyclass_sync() + | ------------------- required by a bound in this function + | where + | T: PyClassSync + Sync, + | ^^^^ required by this bound in `assert_pyclass_sync` + +error[E0277]: `*mut c_void` cannot be shared between threads safely + --> tests/ui/pyclass_send.rs:8:8 + | +8 | struct SendNotSync(*mut c_void); + | ^^^^^^^^^^^ `*mut c_void` cannot be shared between threads safely + | + = help: within `SendNotSync`, the trait `Sync` is not implemented for `*mut c_void` +note: required because it appears within the type `SendNotSync` + --> tests/ui/pyclass_send.rs:8:8 + | +8 | struct SendNotSync(*mut c_void); + | ^^^^^^^^^^^ +note: required by a bound in `assert_pyclass_sync` + --> src/impl_/pyclass/assertions.rs + | + | pub const fn assert_pyclass_sync() + | ------------------- required by a bound in this function + | where + | T: PyClassSync + Sync, + | ^^^^ required by this bound in `assert_pyclass_sync` From 5220a93d777ca5cd64941c5b036c16fef43f7052 Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Mon, 19 May 2025 10:47:58 -0700 Subject: [PATCH 04/16] Add Windows ARM64 Build configuration (#5145) * Add Windows ARM64 Build configuration * Add change notes * add llvm install * fix yml syntax * correct `os` in `if` clause * add llvm version * fix clippy, ignore coverage for now * correct `if` condition --------- Co-authored-by: David Hewitt --- .github/workflows/build.yml | 12 ++++++++++++ .github/workflows/ci.yml | 21 ++++++++++++++++++++- newsfragments/5145.packaging.md | 1 + 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 newsfragments/5145.packaging.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 60744cadbf7..37da958f35a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,6 +69,14 @@ jobs: run: | echo "CARGO_BUILD_TARGET=i686-pc-windows-msvc" >> $GITHUB_ENV + # windows on arm image contains x86-64 libclang + - name: Install LLVM and Clang + if: inputs.os == 'windows-11-arm' + uses: KyleMayes/install-llvm-action@v2 + with: + # to match windows-2022 images + version: "18" + - name: Install zoneinfo backport for Python 3.7 / 3.8 if: contains(fromJSON('["3.7", "3.8"]'), inputs.python-version) run: python -m pip install backports.zoneinfo @@ -132,6 +140,8 @@ jobs: - if: ${{ github.event_name != 'merge_group' }} name: Generate coverage report + # needs investigation why llvm-cov fails on windows-11-arm + continue-on-error: ${{ inputs.os == 'windows-11-arm' }} run: cargo llvm-cov --package=pyo3 --package=pyo3-build-config @@ -143,6 +153,8 @@ jobs: - if: ${{ github.event_name != 'merge_group' }} name: Upload coverage report uses: codecov/codecov-action@v5 + # needs investigation why llvm-cov fails on windows-11-arm + continue-on-error: ${{ inputs.os == 'windows-11-arm' }} with: files: coverage.json name: ${{ inputs.os }}/${{ inputs.python-version }}/${{ inputs.rust }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 428b78e5c9d..70628644447 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,6 +133,11 @@ jobs: python-architecture: "x86", rust-target: "i686-pc-windows-msvc", }, + { + os: "windows-11-arm", + python-architecture: "arm64", + rust-target: "aarch64-pc-windows-msvc", + }, ] include: # Run beta clippy as a way to detect any incoming lints which may affect downstream users @@ -159,6 +164,13 @@ jobs: - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} + # windows on arm image contains x86-64 libclang + - name: Install LLVM and Clang + if: matrix.platform.os == 'windows-11-arm' + uses: KyleMayes/install-llvm-action@v2 + with: + # to match windows-2022 images + version: "18" - run: python -m pip install --upgrade pip && pip install nox - run: nox -s clippy-all env: @@ -385,7 +397,14 @@ jobs: python-architecture: "arm64", rust-target: "aarch64-unknown-linux-gnu", } - + - rust: stable + python-version: "3.13" + platform: + { + os: "windows-11-arm", + python-architecture: "arm64", + rust-target: "aarch64-pc-windows-msvc", + } exclude: # ubuntu-latest (24.04) no longer supports 3.7 - python-version: "3.7" diff --git a/newsfragments/5145.packaging.md b/newsfragments/5145.packaging.md new file mode 100644 index 00000000000..a11116acc28 --- /dev/null +++ b/newsfragments/5145.packaging.md @@ -0,0 +1 @@ +add support for Windows on ARM64. \ No newline at end of file From bc7c57294b30669c6006ae9d0e5bf3d2fa43baf4 Mon Sep 17 00:00:00 2001 From: yogevm15 <45556766+yogevm15@users.noreply.github.com> Date: Tue, 20 May 2025 00:05:14 +0200 Subject: [PATCH 05/16] Introspection: modules associated constants (#5150) * Adds introspection for modules associated constants * Add support for `Final` constants. * Document changes * Review fixes * Fix typo --- newsfragments/5150.added.md | 1 + pyo3-introspection/src/introspection.rs | 23 ++++++++++++++-- pyo3-introspection/src/model.rs | 7 +++++ pyo3-introspection/src/stubs.rs | 26 ++++++++++++++++-- pyo3-macros-backend/src/introspection.rs | 35 ++++++++++++++++++++++++ pyo3-macros-backend/src/method.rs | 28 ++----------------- pyo3-macros-backend/src/module.rs | 10 +++++-- pyo3-macros-backend/src/utils.rs | 28 +++++++++++++++++++ pytests/src/consts.rs | 10 +++++++ pytests/src/lib.rs | 3 +- pytests/stubs/consts.pyi | 4 +++ 11 files changed, 141 insertions(+), 34 deletions(-) create mode 100644 newsfragments/5150.added.md create mode 100644 pytests/src/consts.rs create mode 100644 pytests/stubs/consts.pyi diff --git a/newsfragments/5150.added.md b/newsfragments/5150.added.md new file mode 100644 index 00000000000..dec859ce549 --- /dev/null +++ b/newsfragments/5150.added.md @@ -0,0 +1 @@ +- Add support for module associated consts introspection. \ No newline at end of file diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index 665109d0a66..b6d9bfb6e18 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -1,4 +1,4 @@ -use crate::model::{Argument, Arguments, Class, Function, Module, VariableLengthArgument}; +use crate::model::{Argument, Arguments, Class, Const, Function, Module, VariableLengthArgument}; use anyhow::{bail, ensure, Context, Result}; use goblin::elf::Elf; use goblin::mach::load_command::CommandVariant; @@ -44,11 +44,12 @@ fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result { if let Chunk::Module { name, members, + consts, id: _, } = chunk { if name == main_module_name { - return convert_module(name, members, &chunks_by_id, &chunks_by_parent); + return convert_module(name, members, consts, &chunks_by_id, &chunks_by_parent); } } } @@ -58,6 +59,7 @@ fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result { fn convert_module( name: &str, members: &[String], + consts: &[ConstChunk], chunks_by_id: &HashMap<&str, &Chunk>, chunks_by_parent: &HashMap<&str, Vec<&Chunk>>, ) -> Result { @@ -69,11 +71,19 @@ fn convert_module( chunks_by_id, chunks_by_parent, )?; + Ok(Module { name: name.into(), modules, classes, functions, + consts: consts + .iter() + .map(|c| Const { + name: c.name.clone(), + value: c.value.clone(), + }) + .collect(), }) } @@ -91,11 +101,13 @@ fn convert_members( Chunk::Module { name, members, + consts, id: _, } => { modules.push(convert_module( name, members, + consts, chunks_by_id, chunks_by_parent, )?); @@ -354,6 +366,7 @@ enum Chunk { id: String, name: String, members: Vec, + consts: Vec, }, Class { id: String, @@ -371,6 +384,12 @@ enum Chunk { }, } +#[derive(Deserialize)] +struct ConstChunk { + name: String, + value: String, +} + #[derive(Deserialize)] struct ChunkArguments { #[serde(default)] diff --git a/pyo3-introspection/src/model.rs b/pyo3-introspection/src/model.rs index 021475b9392..dc98198ca90 100644 --- a/pyo3-introspection/src/model.rs +++ b/pyo3-introspection/src/model.rs @@ -4,6 +4,7 @@ pub struct Module { pub modules: Vec, pub classes: Vec, pub functions: Vec, + pub consts: Vec, } #[derive(Debug, Eq, PartialEq, Clone, Hash)] @@ -20,6 +21,12 @@ pub struct Function { pub arguments: Arguments, } +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub struct Const { + pub name: String, + pub value: String, +} + #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub struct Arguments { /// Arguments before / diff --git a/pyo3-introspection/src/stubs.rs b/pyo3-introspection/src/stubs.rs index cc0a11ebd38..8be82da1f11 100644 --- a/pyo3-introspection/src/stubs.rs +++ b/pyo3-introspection/src/stubs.rs @@ -1,5 +1,5 @@ -use crate::model::{Argument, Class, Function, Module, VariableLengthArgument}; -use std::collections::HashMap; +use crate::model::{Argument, Class, Const, Function, Module, VariableLengthArgument}; +use std::collections::{BTreeSet, HashMap}; use std::path::{Path, PathBuf}; /// Generates the [type stubs](https://typing.readthedocs.io/en/latest/source/stubs.html) of a given module. @@ -32,6 +32,7 @@ fn add_module_stub_files( /// Generates the module stubs to a String, not including submodules fn module_stubs(module: &Module) -> String { + let mut modules_to_import = BTreeSet::new(); let mut elements = Vec::new(); for class in &module.classes { elements.push(class_stubs(class)); @@ -39,9 +40,21 @@ fn module_stubs(module: &Module) -> String { for function in &module.functions { elements.push(function_stubs(function)); } + for konst in &module.consts { + elements.push(const_stubs(konst, &mut modules_to_import)); + } - // We insert two line jumps (i.e. empty strings) only above and below multiple line elements (classes with methods, functions with decorators) let mut output = String::new(); + + for module_to_import in &modules_to_import { + output.push_str(&format!("import {module_to_import}\n")); + } + + if !modules_to_import.is_empty() { + output.push('\n') + } + + // We insert two line jumps (i.e. empty strings) only above and below multiple line elements (classes with methods, functions with decorators) for element in elements { let is_multiline = element.contains('\n'); if is_multiline && !output.is_empty() && !output.ends_with("\n\n") { @@ -53,6 +66,7 @@ fn module_stubs(module: &Module) -> String { output.push('\n'); } } + // We remove a line jump at the end if they are two if output.ends_with("\n\n") { output.pop(); @@ -111,6 +125,12 @@ fn function_stubs(function: &Function) -> String { buffer } +fn const_stubs(konst: &Const, modules_to_import: &mut BTreeSet) -> String { + modules_to_import.insert("typing".to_string()); + let Const { name, value } = konst; + format!("{name}: typing.Final = {value}") +} + fn argument_stub(argument: &Argument) -> String { let mut output = argument.name.clone(); if let Some(default_value) = &argument.default_value { diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index 303e952b814..dcb76e668b1 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -19,6 +19,7 @@ use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::mem::take; use std::sync::atomic::{AtomicUsize, Ordering}; +use syn::ext::IdentExt; use syn::{Attribute, Ident, Type, TypePath}; static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0); @@ -28,6 +29,9 @@ pub fn module_introspection_code<'a>( name: &str, members: impl IntoIterator, members_cfg_attrs: impl IntoIterator>, + consts: impl IntoIterator, + consts_values: impl IntoIterator, + consts_cfg_attrs: impl IntoIterator>, ) -> TokenStream { IntrospectionNode::Map( [ @@ -52,6 +56,23 @@ pub fn module_introspection_code<'a>( .collect(), ), ), + ( + "consts", + IntrospectionNode::List( + consts + .into_iter() + .zip(consts_values) + .zip(consts_cfg_attrs) + .filter_map(|((ident, value), attributes)| { + if attributes.is_empty() { + Some(const_introspection_code(ident, value)) + } else { + None // TODO: properly interpret cfg attributes + } + }) + .collect(), + ), + ), ] .into(), ) @@ -116,6 +137,20 @@ pub fn function_introspection_code( IntrospectionNode::Map(desc).emit(pyo3_crate_path) } +fn const_introspection_code<'a>(ident: &'a Ident, value: &'a String) -> IntrospectionNode<'a> { + IntrospectionNode::Map( + [ + ("type", IntrospectionNode::String("const".into())), + ( + "name", + IntrospectionNode::String(ident.unraw().to_string().into()), + ), + ("value", IntrospectionNode::String(value.into())), + ] + .into(), + ) +} + fn arguments_introspection_data<'a>( signature: &'a FunctionSignature<'a>, first_argument: Option<&'a str>, diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 0ab5135e93a..911ad58fdcf 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -7,7 +7,7 @@ use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::{ext::IdentExt, spanned::Spanned, Ident, Result}; use crate::pyversions::is_abi3_before; -use crate::utils::{Ctx, LitCStr}; +use crate::utils::{expr_to_python, Ctx, LitCStr}; use crate::{ attributes::{FromPyWithAttribute, TextSignatureAttribute, TextSignatureAttributeValue}, params::{impl_arg_params, Holders}, @@ -34,31 +34,7 @@ impl RegularArg<'_> { .. } = self { - match arg_default { - // literal values - syn::Expr::Lit(syn::ExprLit { lit, .. }) => match lit { - syn::Lit::Str(s) => s.token().to_string(), - syn::Lit::Char(c) => c.token().to_string(), - syn::Lit::Int(i) => i.base10_digits().to_string(), - syn::Lit::Float(f) => f.base10_digits().to_string(), - syn::Lit::Bool(b) => { - if b.value() { - "True".to_string() - } else { - "False".to_string() - } - } - _ => "...".to_string(), - }, - // None - syn::Expr::Path(syn::ExprPath { qself, path, .. }) - if qself.is_none() && path.is_ident("None") => - { - "None".to_string() - } - // others, unsupported yet so defaults to `...` - _ => "...".to_string(), - } + expr_to_python(arg_default) } else if let RegularArg { option_wrapped_type: Some(..), .. diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 860a3b6d857..dd22778465b 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -2,6 +2,7 @@ #[cfg(feature = "experimental-inspect")] use crate::introspection::{introspection_id_const, module_introspection_code}; +use crate::utils::expr_to_python; use crate::{ attributes::{ self, kw, take_attributes, take_pyo3_options, CrateAttribute, GILUsedAttribute, @@ -150,6 +151,7 @@ pub fn pymodule_module_impl( let mut pymodule_init = None; let mut module_consts = Vec::new(); + let mut module_consts_values = Vec::new(); let mut module_consts_cfg_attrs = Vec::new(); for item in &mut *items { @@ -293,8 +295,8 @@ pub fn pymodule_module_impl( if !find_and_remove_attribute(&mut item.attrs, "pymodule_export") { continue; } - module_consts.push(item.ident.clone()); + module_consts_values.push(expr_to_python(&item.expr)); module_consts_cfg_attrs.push(get_cfg_attributes(&item.attrs)); } Item::Static(item) => { @@ -349,6 +351,9 @@ pub fn pymodule_module_impl( &name.to_string(), &module_items, &module_items_cfg_attrs, + &module_consts, + &module_consts_values, + &module_consts_cfg_attrs, ); #[cfg(not(feature = "experimental-inspect"))] let introspection = quote! {}; @@ -432,7 +437,8 @@ pub fn pymodule_function_impl( ); #[cfg(feature = "experimental-inspect")] - let introspection = module_introspection_code(pyo3_path, &name.to_string(), &[], &[]); + let introspection = + module_introspection_code(pyo3_path, &name.to_string(), &[], &[], &[], &[], &[]); #[cfg(not(feature = "experimental-inspect"))] let introspection = quote! {}; #[cfg(feature = "experimental-inspect")] diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 09f86158834..0f4a267cac7 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -393,3 +393,31 @@ impl TypeExt for syn::Type { self } } + +pub fn expr_to_python(expr: &syn::Expr) -> String { + match expr { + // literal values + syn::Expr::Lit(syn::ExprLit { lit, .. }) => match lit { + syn::Lit::Str(s) => s.token().to_string(), + syn::Lit::Char(c) => c.token().to_string(), + syn::Lit::Int(i) => i.base10_digits().to_string(), + syn::Lit::Float(f) => f.base10_digits().to_string(), + syn::Lit::Bool(b) => { + if b.value() { + "True".to_string() + } else { + "False".to_string() + } + } + _ => "...".to_string(), + }, + // None + syn::Expr::Path(syn::ExprPath { qself, path, .. }) + if qself.is_none() && path.is_ident("None") => + { + "None".to_string() + } + // others, unsupported yet so defaults to `...` + _ => "...".to_string(), + } +} diff --git a/pytests/src/consts.rs b/pytests/src/consts.rs new file mode 100644 index 00000000000..a0bdf3a9d3e --- /dev/null +++ b/pytests/src/consts.rs @@ -0,0 +1,10 @@ +use pyo3::pymodule; + +#[pymodule] +pub mod consts { + #[pymodule_export] + pub const PI: f64 = std::f64::consts::PI; // Exports PI constant as part of the module + + #[pymodule_export] + pub const SIMPLE: &str = "SIMPLE"; +} diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index 4112c90400e..9a406d9fe05 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -5,6 +5,7 @@ use pyo3::wrap_pymodule; pub mod awaitable; pub mod buf_and_str; pub mod comparisons; +mod consts; pub mod datetime; pub mod dict_iter; pub mod enums; @@ -22,7 +23,7 @@ mod pyo3_pytests { use super::*; #[pymodule_export] - use {pyclasses::pyclasses, pyfunctions::pyfunctions}; + use {consts::consts, pyclasses::pyclasses, pyfunctions::pyfunctions}; // Inserting to sys.modules allows importing submodules nicely from Python // e.g. import pyo3_pytests.buf_and_str as bas diff --git a/pytests/stubs/consts.pyi b/pytests/stubs/consts.pyi new file mode 100644 index 00000000000..9cf89e44883 --- /dev/null +++ b/pytests/stubs/consts.pyi @@ -0,0 +1,4 @@ +import typing + +PI: typing.Final = ... +SIMPLE: typing.Final = "SIMPLE" From ef67be3f73bc501023b1094630d550b1d26a468d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 17:57:57 +0200 Subject: [PATCH 06/16] build(deps): update criterion requirement in /pyo3-benches (#5155) Updates the requirements on [criterion](https://github.com/bheisler/criterion.rs) to permit the latest version. - [Changelog](https://github.com/bheisler/criterion.rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/bheisler/criterion.rs/compare/0.5.1...0.6.0) --- updated-dependencies: - dependency-name: criterion dependency-version: 0.6.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyo3-benches/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyo3-benches/Cargo.toml b/pyo3-benches/Cargo.toml index 24ec9b5d76e..9f0c4c67b08 100644 --- a/pyo3-benches/Cargo.toml +++ b/pyo3-benches/Cargo.toml @@ -14,7 +14,7 @@ pyo3-build-config = { path = "../pyo3-build-config" } [dev-dependencies] codspeed-criterion-compat = "2.3" -criterion = "0.5.1" +criterion = "0.6.0" num-bigint = "0.4.3" rust_decimal = { version = "1.0.0", default-features = false } hashbrown = "0.15" From 2c13c1c0a78586fd32d84c5cbed7e45b119f9d04 Mon Sep 17 00:00:00 2001 From: Cheuk Ting Ho Date: Tue, 20 May 2025 22:35:07 +0100 Subject: [PATCH 07/16] Catching Invalid Asyncs (#5156) * Catching invalid asyncs * Catching invalid asyncs (adding missing file) * formating * adding changelog file * Update newsfragments/5156.fixed.md Co-authored-by: Daniel McCarney * make sure no experimental-async feature * revert pytest/test_awaitable.py changes * make sure not using experimental-async feature --------- Co-authored-by: Daniel McCarney --- newsfragments/5156.fixed.md | 1 + pyo3-macros-backend/src/method.rs | 7 ------- pyo3-macros-backend/src/pyfunction.rs | 6 ++++++ pyo3-macros-backend/src/pymethod.rs | 7 +++++++ tests/test_compile_error.rs | 2 ++ tests/ui/invalid_async.rs | 20 ++++++++++++++++++++ tests/ui/invalid_async.stderr | 11 +++++++++++ 7 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 newsfragments/5156.fixed.md create mode 100644 tests/ui/invalid_async.rs create mode 100644 tests/ui/invalid_async.stderr diff --git a/newsfragments/5156.fixed.md b/newsfragments/5156.fixed.md new file mode 100644 index 00000000000..2c6049ef855 --- /dev/null +++ b/newsfragments/5156.fixed.md @@ -0,0 +1 @@ +Catching `async` declaration when not using `experimental-async` \ No newline at end of file diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 911ad58fdcf..d2ddb3179a4 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -669,13 +669,6 @@ impl<'a> FnSpec<'a> { } } - if self.asyncness.is_some() { - ensure_spanned!( - cfg!(feature = "experimental-async"), - self.asyncness.span() => "async functions are only supported with the `experimental-async` feature" - ); - } - let rust_call = |args: Vec, holders: &mut Holders| { let mut self_arg = || self.tp.self_arg(cls, ExtractErrorMode::Raise, holders, ctx); diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index e512ca1cabc..82d9bceabb8 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -258,6 +258,12 @@ pub fn impl_wrap_pyfunction( }; let wrapper_ident = format_ident!("__pyfunction_{}", spec.name); + if spec.asyncness.is_some() { + ensure_spanned!( + cfg!(feature = "experimental-async"), + spec.asyncness.span() => "async functions are only supported with the `experimental-async` feature" + ); + } let wrapper = spec.get_wrapper_function(&wrapper_ident, None, ctx)?; let methoddef = spec.get_methoddef(wrapper_ident, &spec.get_doc(&func.attrs, ctx), ctx); diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 434a96e9dde..a34e22b1e3d 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -196,6 +196,13 @@ pub fn gen_py_method( let spec = &method.spec; let Ctx { pyo3_path, .. } = ctx; + if spec.asyncness.is_some() { + ensure_spanned!( + cfg!(feature = "experimental-async"), + spec.asyncness.span() => "async functions are only supported with the `experimental-async` feature" + ); + } + Ok(match (method.kind, &spec.tp) { // Class attributes go before protos so that class attributes can be used to set proto // method to None. diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index c6990430366..0053f86e7bb 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -41,6 +41,8 @@ fn test_compile_errors() { // output changes with async feature #[cfg(all(Py_LIMITED_API, feature = "experimental-async"))] t.compile_fail("tests/ui/abi3_nativetype_inheritance.rs"); + #[cfg(not(any(feature = "experimental-async")))] + t.compile_fail("tests/ui/invalid_async.rs"); t.compile_fail("tests/ui/invalid_intern_arg.rs"); t.compile_fail("tests/ui/invalid_frozen_pyclass_borrow.rs"); #[cfg(not(any(feature = "hashbrown", feature = "indexmap")))] diff --git a/tests/ui/invalid_async.rs b/tests/ui/invalid_async.rs new file mode 100644 index 00000000000..2c7375fd2f9 --- /dev/null +++ b/tests/ui/invalid_async.rs @@ -0,0 +1,20 @@ +use pyo3::prelude::*; + +#[pyfunction] +async fn check(){} + +#[pyclass] +pub(crate) struct AsyncRange { + count: i32, + target: i32, +} +#[pymethods] +impl AsyncRange { + async fn __anext__(mut _pyself: PyRefMut<'_, Self>) -> PyResult { + Ok(0) + } + + async fn foo(&self) {} +} + +fn main() {} \ No newline at end of file diff --git a/tests/ui/invalid_async.stderr b/tests/ui/invalid_async.stderr new file mode 100644 index 00000000000..5573cdf388a --- /dev/null +++ b/tests/ui/invalid_async.stderr @@ -0,0 +1,11 @@ +error: async functions are only supported with the `experimental-async` feature + --> tests/ui/invalid_async.rs:4:1 + | +4 | async fn check(){} + | ^^^^^ + +error: async functions are only supported with the `experimental-async` feature + --> tests/ui/invalid_async.rs:13:5 + | +13 | async fn __anext__(mut _pyself: PyRefMut<'_, Self>) -> PyResult { + | ^^^^^ From a27ccd7b4ad73ca99ba14c27c8ace74fabda2ee5 Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Fri, 23 May 2025 16:57:24 +0200 Subject: [PATCH 08/16] Updates to better support ormsgpack on GraalPy. (#5121) * Expose the vectorcall functions also on GraalPy * Add PyBytes_AS_STRING, seen in ormsgpack * Call Py_Is function on GraalPy --- newsfragments/5121.added.md | 1 + newsfragments/5121.changed.md | 2 ++ pyo3-ffi/src/abstract_.rs | 3 +-- pyo3-ffi/src/compat/py_3_9.rs | 3 +-- pyo3-ffi/src/cpython/abstract_.rs | 22 +++++++++++----------- pyo3-ffi/src/cpython/bytesobject.rs | 11 ++++++++++- pyo3-ffi/src/object.rs | 4 ++-- src/types/bytes.rs | 15 +++++++++++++++ 8 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 newsfragments/5121.added.md create mode 100644 newsfragments/5121.changed.md diff --git a/newsfragments/5121.added.md b/newsfragments/5121.added.md new file mode 100644 index 00000000000..fd79e5a8ea7 --- /dev/null +++ b/newsfragments/5121.added.md @@ -0,0 +1 @@ +Add `PyBytes_AS_STRING` diff --git a/newsfragments/5121.changed.md b/newsfragments/5121.changed.md new file mode 100644 index 00000000000..49b317cb96b --- /dev/null +++ b/newsfragments/5121.changed.md @@ -0,0 +1,2 @@ +Enable vectorcall methods on GraalPy +Call Py_Is function on GraalPy diff --git a/pyo3-ffi/src/abstract_.rs b/pyo3-ffi/src/abstract_.rs index 84fb98a1b4e..59639d14de8 100644 --- a/pyo3-ffi/src/abstract_.rs +++ b/pyo3-ffi/src/abstract_.rs @@ -25,7 +25,6 @@ pub unsafe fn PyObject_DelAttr(o: *mut PyObject, attr_name: *mut PyObject) -> c_ extern "C" { #[cfg(all( not(PyPy), - not(GraalPy), any(Py_3_10, all(not(Py_LIMITED_API), Py_3_9)) // Added to python in 3.9 but to limited API in 3.10 ))] #[cfg_attr(PyPy, link_name = "PyPyObject_CallNoArgs")] @@ -94,7 +93,7 @@ extern "C" { kwnames: *mut PyObject, ) -> *mut PyObject; - #[cfg(any(Py_3_12, all(Py_3_9, not(any(Py_LIMITED_API, PyPy, GraalPy)))))] + #[cfg(any(Py_3_12, all(Py_3_9, not(any(Py_LIMITED_API, PyPy)))))] pub fn PyObject_VectorcallMethod( name: *mut PyObject, args: *const *mut PyObject, diff --git a/pyo3-ffi/src/compat/py_3_9.rs b/pyo3-ffi/src/compat/py_3_9.rs index 285f2b2ae7e..6b3521cc167 100644 --- a/pyo3-ffi/src/compat/py_3_9.rs +++ b/pyo3-ffi/src/compat/py_3_9.rs @@ -1,7 +1,6 @@ compat_function!( originally_defined_for(all( not(PyPy), - not(GraalPy), any(Py_3_10, all(not(Py_LIMITED_API), Py_3_9)) // Added to python in 3.9 but to limited API in 3.10 )); @@ -12,7 +11,7 @@ compat_function!( ); compat_function!( - originally_defined_for(all(Py_3_9, not(any(Py_LIMITED_API, PyPy, GraalPy)))); + originally_defined_for(all(Py_3_9, not(any(Py_LIMITED_API, PyPy)))); #[inline] pub unsafe fn PyObject_CallMethodNoArgs(obj: *mut crate::PyObject, name: *mut crate::PyObject) -> *mut crate::PyObject { diff --git a/pyo3-ffi/src/cpython/abstract_.rs b/pyo3-ffi/src/cpython/abstract_.rs index 6ada1a754ef..b9d9dd47dc9 100644 --- a/pyo3-ffi/src/cpython/abstract_.rs +++ b/pyo3-ffi/src/cpython/abstract_.rs @@ -1,12 +1,12 @@ use crate::{PyObject, Py_ssize_t}; -#[cfg(any(all(Py_3_8, not(any(PyPy, GraalPy))), not(Py_3_11)))] +#[cfg(any(all(Py_3_8, not(PyPy)), not(Py_3_11)))] use std::os::raw::c_char; use std::os::raw::c_int; #[cfg(not(Py_3_11))] use crate::Py_buffer; -#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] +#[cfg(all(Py_3_8, not(PyPy)))] use crate::{ vectorcallfunc, PyCallable_Check, PyThreadState, PyThreadState_GET, PyTuple_Check, PyType_HasFeature, Py_TPFLAGS_HAVE_VECTORCALL, @@ -23,7 +23,7 @@ extern "C" { const _PY_FASTCALL_SMALL_STACK: size_t = 5; extern "C" { - #[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] + #[cfg(all(Py_3_8, not(PyPy)))] pub fn _Py_CheckFunctionResult( tstate: *mut PyThreadState, callable: *mut PyObject, @@ -31,7 +31,7 @@ extern "C" { where_: *const c_char, ) -> *mut PyObject; - #[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] + #[cfg(all(Py_3_8, not(PyPy)))] pub fn _PyObject_MakeTpCall( tstate: *mut PyThreadState, callable: *mut PyObject, @@ -52,7 +52,7 @@ pub unsafe fn PyVectorcall_NARGS(n: size_t) -> Py_ssize_t { n.try_into().expect("cannot fail due to mask") } -#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] +#[cfg(all(Py_3_8, not(PyPy)))] #[inline(always)] pub unsafe fn PyVectorcall_Function(callable: *mut PyObject) -> Option { assert!(!callable.is_null()); @@ -67,7 +67,7 @@ pub unsafe fn PyVectorcall_Function(callable: *mut PyObject) -> Option *mut PyObject { _PyObject_VectorcallTstate( @@ -166,7 +166,7 @@ extern "C" { pub fn _PyObject_CallNoArg(func: *mut PyObject) -> *mut PyObject; } -#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] +#[cfg(all(Py_3_8, not(PyPy)))] #[inline(always)] pub unsafe fn PyObject_CallOneArg(func: *mut PyObject, arg: *mut PyObject) -> *mut PyObject { assert!(!arg.is_null()); @@ -177,7 +177,7 @@ pub unsafe fn PyObject_CallOneArg(func: *mut PyObject, arg: *mut PyObject) -> *m _PyObject_VectorcallTstate(tstate, func, args, nargsf, std::ptr::null_mut()) } -#[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))] +#[cfg(all(Py_3_9, not(PyPy)))] #[inline(always)] pub unsafe fn PyObject_CallMethodNoArgs( self_: *mut PyObject, @@ -191,7 +191,7 @@ pub unsafe fn PyObject_CallMethodNoArgs( ) } -#[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))] +#[cfg(all(Py_3_9, not(PyPy)))] #[inline(always)] pub unsafe fn PyObject_CallMethodOneArg( self_: *mut PyObject, @@ -219,7 +219,7 @@ extern "C" { pub fn PyObject_LengthHint(o: *mut PyObject, arg1: Py_ssize_t) -> Py_ssize_t; #[cfg(not(Py_3_11))] // moved to src/buffer.rs from 3.11 - #[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))] + #[cfg(all(Py_3_9, not(PyPy)))] pub fn PyObject_CheckBuffer(obj: *mut PyObject) -> c_int; } diff --git a/pyo3-ffi/src/cpython/bytesobject.rs b/pyo3-ffi/src/cpython/bytesobject.rs index dd78e646a12..9e233311ac1 100644 --- a/pyo3-ffi/src/cpython/bytesobject.rs +++ b/pyo3-ffi/src/cpython/bytesobject.rs @@ -1,6 +1,6 @@ use crate::object::*; use crate::Py_ssize_t; -#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] +#[cfg(not(Py_LIMITED_API))] use std::os::raw::c_char; use std::os::raw::c_int; @@ -23,3 +23,12 @@ extern "C" { #[cfg_attr(PyPy, link_name = "_PyPyBytes_Resize")] pub fn _PyBytes_Resize(bytes: *mut *mut PyObject, newsize: Py_ssize_t) -> c_int; } + +#[cfg(not(Py_LIMITED_API))] +#[inline] +pub unsafe fn PyBytes_AS_STRING(op: *mut PyObject) -> *const c_char { + #[cfg(not(any(PyPy, GraalPy)))] + return &(*op.cast::()).ob_sval as *const c_char; + #[cfg(any(PyPy, GraalPy))] + return crate::PyBytes_AsString(op); +} diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 5fbf45db617..c4c5abc361c 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -147,13 +147,13 @@ pub struct PyVarObject { // skipped private _PyVarObject_CAST #[inline] -#[cfg(not(all(PyPy, Py_3_10)))] +#[cfg(not(any(GraalPy, all(PyPy, Py_3_10))))] #[cfg_attr(docsrs, doc(cfg(all())))] pub unsafe fn Py_Is(x: *mut PyObject, y: *mut PyObject) -> c_int { (x == y).into() } -#[cfg(all(PyPy, Py_3_10))] +#[cfg(any(GraalPy, all(PyPy, Py_3_10)))] #[cfg_attr(docsrs, doc(cfg(all())))] extern "C" { #[cfg_attr(PyPy, link_name = "PyPy_Is")] diff --git a/src/types/bytes.rs b/src/types/bytes.rs index 4e2ce9c96e7..16e0c42f8b1 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -365,4 +365,19 @@ mod tests { assert_eq!(*b, py_string); }) } + + #[test] + #[cfg(not(Py_LIMITED_API))] + fn test_as_string() { + Python::with_gil(|py| { + let b = b"hello, world".as_slice(); + let py_bytes = PyBytes::new(py, b); + unsafe { + assert_eq!( + ffi::PyBytes_AsString(py_bytes.as_ptr()) as *const std::os::raw::c_char, + ffi::PyBytes_AS_STRING(py_bytes.as_ptr()) as *const std::os::raw::c_char + ); + } + }) + } } From 008efe221d2435763bab9e6e334516ac643d8bca Mon Sep 17 00:00:00 2001 From: Fabio Valentini Date: Mon, 26 May 2025 12:16:51 +0200 Subject: [PATCH 09/16] conversions: fix FromPyObject impl for uuid::Uuid for big-endian (#5161) The "to_le()" conversion on the u128 seems to be just wrong. --- newsfragments/5161.fixed.md | 1 + src/conversions/uuid.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 newsfragments/5161.fixed.md diff --git a/newsfragments/5161.fixed.md b/newsfragments/5161.fixed.md new file mode 100644 index 00000000000..1a40c79bbe8 --- /dev/null +++ b/newsfragments/5161.fixed.md @@ -0,0 +1 @@ +Fixed FromPyObject impl for `uuid::Uuid` on big-endian architectures diff --git a/src/conversions/uuid.rs b/src/conversions/uuid.rs index 44ac0bae25a..116bc1e2b29 100644 --- a/src/conversions/uuid.rs +++ b/src/conversions/uuid.rs @@ -85,7 +85,7 @@ impl FromPyObject<'_> for Uuid { if obj.is_instance(uuid_cls)? { let uuid_int: u128 = obj.getattr(intern!(py, "int"))?.extract()?; - Ok(Uuid::from_u128(uuid_int.to_le())) + Ok(Uuid::from_u128(uuid_int)) } else { Err(PyTypeError::new_err("Expected a `uuid.UUID` instance.")) } From 5db48718bdd5e7f6611db65298cdb83e53493a7c Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 26 May 2025 12:29:12 -0600 Subject: [PATCH 10/16] Remove unnecessary warning filter in pytests (#5165) --- pytests/pytest.ini | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 pytests/pytest.ini diff --git a/pytests/pytest.ini b/pytests/pytest.ini deleted file mode 100644 index 3d62037f722..00000000000 --- a/pytests/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -# see https://github.com/PyO3/pyo3/issues/5094 for details -[pytest] -filterwarnings = ignore::DeprecationWarning:pytest_asyncio.* \ No newline at end of file From 2829027f9c8a9b698f86bfe78937c38bc8d3b0ca Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Tue, 3 Jun 2025 09:59:27 +0200 Subject: [PATCH 11/16] Add conversions for chrono's `Local` timezone (#5174) --- Cargo.toml | 6 ++- guide/src/features.md | 4 ++ newsfragments/5174.added.md | 1 + src/conversions/chrono.rs | 98 ++++++++++++++++++++++++++++++++++++- 4 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 newsfragments/5174.added.md diff --git a/Cargo.toml b/Cargo.toml index 55e4a29170c..c4a97f0ab3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,8 @@ serde = { version = "1.0", optional = true } smallvec = { version = "1.0", optional = true } uuid = { version = "1.11.0", optional = true } lock_api = { version = "0.4", optional = true } -parking_lot = { version = "0.12", optional = true} +parking_lot = { version = "0.12", optional = true } +iana-time-zone = { version = "0.1", optional = true, features = ["fallback"]} [target.'cfg(not(target_has_atomic = "64"))'.dependencies] portable-atomic = "1.0" @@ -121,6 +122,8 @@ py-clone = [] parking_lot = ["dep:parking_lot", "lock_api"] arc_lock = ["lock_api", "lock_api/arc_lock", "parking_lot?/arc_lock"] +chrono-local = ["chrono/clock", "dep:iana-time-zone"] + # Optimizes PyObject to Vec conversion and so on. nightly = [] @@ -133,6 +136,7 @@ full = [ "arc_lock", "bigdecimal", "chrono", + "chrono-local", "chrono-tz", "either", "experimental-async", diff --git a/guide/src/features.md b/guide/src/features.md index 450c1b4ea04..aad151baa58 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -138,6 +138,10 @@ Adds a dependency on [chrono](https://docs.rs/chrono). Enables a conversion from - [NaiveTime](https://docs.rs/chrono/latest/chrono/naive/struct.NaiveTime.html) -> [`PyTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTime.html) - [DateTime](https://docs.rs/chrono/latest/chrono/struct.DateTime.html) -> [`PyDateTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDateTime.html) +### `chrono-local` + +Enables conversion from and to [Local](https://docs.rs/chrono/latest/chrono/struct.Local.html) timezones. + ### `chrono-tz` Adds a dependency on [chrono-tz](https://docs.rs/chrono-tz). diff --git a/newsfragments/5174.added.md b/newsfragments/5174.added.md new file mode 100644 index 00000000000..09b7dec02e7 --- /dev/null +++ b/newsfragments/5174.added.md @@ -0,0 +1 @@ +Add conversions for chrono's `Local` timezone & `DateTime` instances. diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index bf99951e459..fab734422f2 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -43,15 +43,23 @@ use crate::conversion::IntoPyObject; use crate::exceptions::{PyTypeError, PyUserWarning, PyValueError}; -#[cfg(Py_LIMITED_API)] use crate::intern; use crate::types::any::PyAnyMethods; use crate::types::PyNone; use crate::types::{PyDate, PyDateTime, PyDelta, PyTime, PyTzInfo, PyTzInfoAccess}; #[cfg(not(Py_LIMITED_API))] use crate::types::{PyDateAccess, PyDeltaAccess, PyTimeAccess}; +#[cfg(feature = "chrono-local")] +use crate::{ + exceptions::PyRuntimeError, + sync::GILOnceCell, + types::{PyString, PyStringMethods}, + Py, +}; use crate::{ffi, Borrowed, Bound, FromPyObject, IntoPyObjectExt, PyAny, PyErr, PyResult, Python}; use chrono::offset::{FixedOffset, Utc}; +#[cfg(feature = "chrono-local")] +use chrono::Local; use chrono::{ DateTime, Datelike, Duration, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Timelike, @@ -387,7 +395,8 @@ impl FromPyObject<'_> for FixedOffset { // Any other timezone would require a datetime as the parameter, and return // None if the datetime is not provided. // Trying to convert None to a PyDelta in the next line will then fail. - let py_timedelta = ob.call_method1("utcoffset", (PyNone::get(ob.py()),))?; + let py_timedelta = + ob.call_method1(intern!(ob.py(), "utcoffset"), (PyNone::get(ob.py()),))?; if py_timedelta.is_none() { return Err(PyTypeError::new_err(format!( "{ob:?} is not a fixed offset timezone" @@ -433,6 +442,54 @@ impl FromPyObject<'_> for Utc { } } +#[cfg(feature = "chrono-local")] +impl<'py> IntoPyObject<'py> for Local { + type Target = PyTzInfo; + type Output = Borrowed<'static, 'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + static LOCAL_TZ: GILOnceCell> = GILOnceCell::new(); + let tz = LOCAL_TZ + .get_or_try_init(py, || { + let iana_name = iana_time_zone::get_timezone().map_err(|e| { + PyRuntimeError::new_err(format!("Could not get local timezone: {e}")) + })?; + PyTzInfo::timezone(py, iana_name).map(Bound::unbind) + })? + .bind_borrowed(py); + Ok(tz) + } +} + +#[cfg(feature = "chrono-local")] +impl<'py> IntoPyObject<'py> for &Local { + type Target = PyTzInfo; + type Output = Borrowed<'static, 'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + +#[cfg(feature = "chrono-local")] +impl FromPyObject<'_> for Local { + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { + let local_tz = Local.into_pyobject(ob.py())?; + if ob.eq(local_tz)? { + Ok(Local) + } else { + let name = local_tz.getattr("key")?.downcast_into::()?; + Err(PyValueError::new_err(format!( + "expected local timezone {}", + name.to_cow()? + ))) + } + } +} + struct DateArgs { year: i32, month: u8, @@ -1281,6 +1338,43 @@ mod tests { } }) } + + #[test] + #[cfg(all(feature = "chrono-local", not(target_os = "windows")))] + fn test_local_datetime_roundtrip( + year in 1i32..=9999i32, + month in 1u32..=12u32, + day in 1u32..=31u32, + hour in 0u32..=23u32, + min in 0u32..=59u32, + sec in 0u32..=59u32, + micro in 0u32..=1_999_999u32, + ) { + Python::with_gil(|py| { + let date_opt = NaiveDate::from_ymd_opt(year, month, day); + let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro); + if let (Some(date), Some(time)) = (date_opt, time_opt) { + let dts = match NaiveDateTime::new(date, time).and_local_timezone(Local) { + LocalResult::None => return, + LocalResult::Single(dt) => [Some((dt, false)), None], + LocalResult::Ambiguous(dt1, dt2) => [Some((dt1, false)), Some((dt2, true))], + }; + for (dt, fold) in dts.iter().filter_map(|input| *input) { + // Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second + let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap(); + let roundtripped: DateTime = py_dt.extract().expect("Round trip"); + // Leap seconds are not roundtripped + let expected_roundtrip_time = micro.checked_sub(1_000_000).map(|micro| NaiveTime::from_hms_micro_opt(hour, min, sec, micro).unwrap()).unwrap_or(time); + let expected_roundtrip_dt: DateTime = if fold { + NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(Local).latest() + } else { + NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(Local).earliest() + }.unwrap(); + assert_eq!(expected_roundtrip_dt, roundtripped); + } + } + }) + } } } } From 7601c0cc1c2465e559602cbd144921f202e451e1 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 6 Jun 2025 13:08:02 +0100 Subject: [PATCH 12/16] ci: fixes 2025-06-06 (#5186) * fix nightly warning of inconsistent lifetime use * workaround Python 3.13.4 ffi-check issue on Windows --- .github/workflows/build.yml | 6 ++++-- .github/workflows/ci.yml | 4 +++- pyo3-macros-backend/src/intopyobject.rs | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 37da958f35a..4b0ec314fd6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,8 +46,7 @@ jobs: python-version: ${{ inputs.python-version }} architecture: ${{ inputs.python-architecture }} # PyPy can have FFI changes within Python versions, which creates pain in CI - # 3.13.2 also had an ABI break so temporarily add this for 3.13 to ensure that we're using 3.13.3 - check-latest: ${{ startsWith(inputs.python-version, 'pypy') || startsWith(inputs.python-version, '3.13') }} + check-latest: ${{ startsWith(inputs.python-version, 'pypy') }} - name: Install nox run: python -m pip install --upgrade pip && pip install nox @@ -111,6 +110,9 @@ jobs: - '.github/workflows/build.yml' - name: Run pyo3-ffi-check + # Python 3.13.4 has an issue on Windows where the headers are configured to always be free-threaded + # https://discuss.python.org/t/heads-up-3-13-5-release-coming-soon/94535 + continue-on-error: ${{ inputs.python-version == '3.13' && (inputs.os == 'windows-latest' || inputs.os == 'windows-11-arm') }} # pypy 3.9 on windows is not PEP 3123 compliant, nor is graalpy if: ${{ endsWith(inputs.python-version, '-dev') || (steps.ffi-changes.outputs.changed == 'true' && inputs.rust == 'stable' && !startsWith(inputs.python-version, 'graalpy') && !(inputs.python-version == 'pypy3.9' && contains(inputs.os, 'windows'))) }} run: nox -s ffi-check diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70628644447..6c7ef5c3763 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,7 +159,9 @@ jobs: components: clippy,rust-src - uses: actions/setup-python@v5 with: - python-version: "3.13" + # FIXME: 3.13.4 has an issue on windows so pinned to 3.13.3 + # once 3.13.5 is out, unpin the patch version + python-version: "3.13.3" architecture: ${{ matrix.platform.python-architecture }} - uses: Swatinem/rust-cache@v2 with: diff --git a/pyo3-macros-backend/src/intopyobject.rs b/pyo3-macros-backend/src/intopyobject.rs index 2532812f6e1..2c12567e8b7 100644 --- a/pyo3-macros-backend/src/intopyobject.rs +++ b/pyo3-macros-backend/src/intopyobject.rs @@ -530,8 +530,8 @@ pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Resu type Error = #error; fn into_pyobject(self, py: #pyo3_path::Python<#lt_param>) -> ::std::result::Result< - ::Output, - ::Error, + >::Output, + >::Error, > { #body } From eb3fac901dc85a101e534a6cae3df50696e51f07 Mon Sep 17 00:00:00 2001 From: jesse Date: Fri, 6 Jun 2025 11:23:39 -0700 Subject: [PATCH 13/16] fix: typos in guide/docs (#5182) --- guide/src/class.md | 6 +++--- guide/src/conversions/traits.md | 4 ++-- guide/src/debugging.md | 2 +- guide/src/features.md | 2 +- guide/src/free-threading.md | 4 ++-- guide/src/migration.md | 6 +++--- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/guide/src/class.md b/guide/src/class.md index 8772e857d7e..321d2e04980 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -55,7 +55,7 @@ enum HttpResponse { } // PyO3 also supports enums with Struct and Tuple variants -// These complex enums have sligtly different behavior from the simple enums above +// These complex enums have slightly different behavior from the simple enums above // They are meant to work with instance checks and match statement patterns // The variants can be mixed and matched // Struct variants have named fields while tuple enums generate generic names for fields in order _0, _1, _2, ... @@ -825,7 +825,7 @@ impl MyClass { ## Classes as function arguments -Free functions defined using `#[pyfunction]` interact with classes through the same mechanisms as the self parameters of instance methods, i.e. they can take GIL-bound references, GIL-bound reference wrappers or GIL-indepedent references: +Free functions defined using `#[pyfunction]` interact with classes through the same mechanisms as the self parameters of instance methods, i.e. they can take GIL-bound references, GIL-bound reference wrappers or GIL-independent references: ```rust,no_run # #![allow(dead_code)] @@ -857,7 +857,7 @@ fn increment_then_print_field(my_class: &Bound<'_, MyClass>) { println!("{}", my_class.borrow().my_field); } -// Take a GIL-indepedent reference when you want to store the reference elsewhere. +// Take a GIL-independent reference when you want to store the reference elsewhere. #[pyfunction] fn print_refcnt(my_class: Py, py: Python<'_>) { println!("{}", my_class.get_refcnt(py)); diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index b1b6cd31127..281116a9747 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -613,7 +613,7 @@ Additionally `IntoPyObject` can be derived for a reference to a struct or enum u ##### `#[derive(IntoPyObject)]`/`#[derive(IntoPyObjectRef)]` Field Attributes - `pyo3(into_py_with = ...)` - apply a custom function to convert the field from Rust into Python. - - the argument must be the function indentifier + - the argument must be the function identifier - the function signature must be `fn(Cow<'_, T>, Python<'py>) -> PyResult>` where `T` is the Rust type of the argument. - `#[derive(IntoPyObject)]` will invoke the function with `Cow::Owned` - `#[derive(IntoPyObjectRef)]` will invoke the function with `Cow::Borrowed` @@ -650,7 +650,7 @@ struct MyPyObjectWrapper(PyObject); impl<'py> IntoPyObject<'py> for MyPyObjectWrapper { type Target = PyAny; // the Python type type Output = Bound<'py, Self::Target>; // in most cases this will be `Bound` - type Error = std::convert::Infallible; // the conversion error type, has to be convertable to `PyErr` + type Error = std::convert::Infallible; // the conversion error type, has to be convertible to `PyErr` fn into_pyobject(self, py: Python<'py>) -> Result { Ok(self.0.into_bound(py)) diff --git a/guide/src/debugging.md b/guide/src/debugging.md index 2cbf867d438..0cee7bd1783 100644 --- a/guide/src/debugging.md +++ b/guide/src/debugging.md @@ -68,7 +68,7 @@ For more information about how to use both `lldb` and `gdb` you can read the [gd ### Debugger specific setup -Depeding on your OS and your preferences you can use two different debuggers, `rust-gdb` or `rust-lldb`. +Depending on your OS and your preferences you can use two different debuggers, `rust-gdb` or `rust-lldb`. {{#tabs }} {{#tab name="Using rust-gdb" }} diff --git a/guide/src/features.md b/guide/src/features.md index aad151baa58..f60b07b6a40 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -77,7 +77,7 @@ This feature was introduced to ease migration. It was found that delayed referen ### `pyo3_disable_reference_pool` -This is a performance-oriented conditional compilation flag, e.g. [set via `$RUSTFLAGS`][set-configuration-options], which disabled the global reference pool and the assocaited overhead for the crossing the Python-Rust boundary. However, if enabled, `Drop`ping an instance of `Py` without the GIL being held will abort the process. +This is a performance-oriented conditional compilation flag, e.g. [set via `$RUSTFLAGS`][set-configuration-options], which disabled the global reference pool and the associated overhead for the crossing the Python-Rust boundary. However, if enabled, `Drop`ping an instance of `Py` without the GIL being held will abort the process. ### `macros` diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 6df9ac01cfb..898755b6118 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -148,7 +148,7 @@ GIL-enabled build instead ask the interpreter to attach the thread to the Python runtime, and there can be many threads simultaneously attached. See [PEP 703](https://peps.python.org/pep-0703/#thread-states) for more background about how threads can be attached and detached from the interpreter runtime, in a -manner analagous to releasing and acquiring the GIL in the GIL-enabled build. +manner analogous to releasing and acquiring the GIL in the GIL-enabled build. In the GIL-enabled build, PyO3 uses the [`Python<'py>`] type and the `'py` lifetime to signify that the global interpreter lock is held. In the @@ -289,7 +289,7 @@ GIL-enabled build. If, for example, the function executed by [`GILOnceCell`] releases the GIL or calls code that releases the GIL, then it is possible for multiple threads to -race to initialize the cell. While the cell will only ever be intialized +race to initialize the cell. While the cell will only ever be initialized once, it can be problematic in some contexts that [`GILOnceCell`] does not block like the standard library [`OnceLock`]. diff --git a/guide/src/migration.md b/guide/src/migration.md index 3beaa5bc07c..ee929e79d2b 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -645,7 +645,7 @@ impl PyClassAsyncIter {
Click to expand -Interactions with Python objects implemented in Rust no longer need to go though `PyCell`. Instead iteractions with Python object now consistently go through `Bound` or `Py` independently of whether `T` is native Python object or a `#[pyclass]` implemented in Rust. Use `Bound::new` or `Py::new` respectively to create and `Bound::borrow(_mut)` / `Py::borrow(_mut)` to borrow the Rust object. +Interactions with Python objects implemented in Rust no longer need to go though `PyCell`. Instead interactions with Python object now consistently go through `Bound` or `Py` independently of whether `T` is native Python object or a `#[pyclass]` implemented in Rust. Use `Bound::new` or `Py::new` respectively to create and `Bound::borrow(_mut)` / `Py::borrow(_mut)` to borrow the Rust object.
### Migrating from the GIL Refs API to `Bound` @@ -657,7 +657,7 @@ To minimise breakage of code using the GIL Refs API, the `Bound` smart pointe To identify what to migrate, temporarily switch off the `gil-refs` feature to see deprecation warnings on [almost](#cases-where-pyo3-cannot-emit-gil-ref-deprecation-warnings) all uses of APIs accepting and producing GIL Refs . Over one or more PRs it should be possible to follow the deprecation hints to update code. Depending on your development environment, switching off the `gil-refs` feature may introduce [some very targeted breakages](#deactivating-the-gil-refs-feature), so you may need to fixup those first. For example, the following APIs have gained updated variants: -- `PyList::new`, `PyTyple::new` and similar constructors have replacements `PyList::new_bound`, `PyTuple::new_bound` etc. +- `PyList::new`, `PyTuple::new` and similar constructors have replacements `PyList::new_bound`, `PyTuple::new_bound` etc. - `FromPyObject::extract` has a new `FromPyObject::extract_bound` (see the section below) - The `PyTypeInfo` trait has had new `_bound` methods added to accept / return `Bound`. @@ -1082,7 +1082,7 @@ impl Object { } } -// It either forces us to release the GIL before aquiring it again. +// It either forces us to release the GIL before acquiring it again. let first = Python::with_gil(|py| Object::new(py)); let second = Python::with_gil(|py| Object::new(py)); drop(first); From 0ba8f31be6afaaf6d63af717aa0192bfdc326534 Mon Sep 17 00:00:00 2001 From: jesse Date: Fri, 6 Jun 2025 11:40:50 -0700 Subject: [PATCH 14/16] docs: add Kyle Barron libraries/packages to README.md (#5172) * readme: add Kyle Barron libraries/packages * remove from readme obstore ref '1' --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 47a74ec84a3..31ed8b7e47a 100644 --- a/README.md +++ b/README.md @@ -182,9 +182,14 @@ about this topic. - [rustimport](https://github.com/mityax/rustimport) _Directly import Rust files or crates from Python, without manual compilation step. Provides pyo3 integration by default and generates pyo3 binding code automatically._ - [pyo3-arrow](https://crates.io/crates/pyo3-arrow) _Lightweight [Apache Arrow](https://arrow.apache.org/) integration for pyo3._ - [pyo3-bytes](https://crates.io/crates/pyo3-bytes) _Integration between [`bytes`](https://crates.io/crates/bytes) and pyo3._ +- [pyo3-object_store](https://github.com/developmentseed/obstore/tree/main/pyo3-object_store) _Integration between [`object_store`](https://docs.rs/object_store) and [`pyo3`](https://github.com/PyO3/pyo3)._ ## Examples +- [arro3](https://github.com/kylebarron/arro3) _A minimal Python library for Apache Arrow, connecting to the Rust arrow crate._ + - [arro3-compute](https://github.com/kylebarron/arro3/tree/main/arro3-compute) _`arro3-compute`_ + - [arro3-core](https://github.com/kylebarron/arro3/tree/main/arro3-core) _`arro3-core`_ + - [arro3-io](https://github.com/kylebarron/arro3/tree/main/arro3-io) _`arro3-io`_ - [bed-reader](https://github.com/fastlmm/bed-reader) _Read and write the PLINK BED format, simply and efficiently._ - Shows Rayon/ndarray::parallel (including capturing errors, controlling thread num), Python types to Rust generics, Github Actions - [blake3-py](https://github.com/oconnor663/blake3-py) _Python bindings for the [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) cryptographic hash function._ @@ -199,6 +204,7 @@ about this topic. - [fastuuid](https://github.com/thedrow/fastuuid/) _Python bindings to Rust's UUID library._ - [feos](https://github.com/feos-org/feos) _Lightning fast thermodynamic modeling in Rust with fully developed Python interface._ - [forust](https://github.com/jinlow/forust) _A lightweight gradient boosted decision tree library written in Rust._ +- [geo-index](https://github.com/kylebarron/geo-index) _A Rust crate and [Python library](https://github.com/kylebarron/geo-index/tree/main/python) for packed, immutable, zero-copy spatial indexes._ - [granian](https://github.com/emmett-framework/granian) _A Rust HTTP server for Python applications._ - [haem](https://github.com/BooleanCat/haem) _A Python library for working on Bioinformatics problems._ - [html2text-rs](https://github.com/deedy5/html2text_rs) _Python library for converting HTML to markup or plain text._ @@ -207,6 +213,7 @@ about this topic. - [johnnycanencrypt](https://github.com/kushaldas/johnnycanencrypt) OpenPGP library with Yubikey support. - [jsonschema](https://github.com/Stranger6667/jsonschema/tree/master/crates/jsonschema-py) _A high-performance JSON Schema validator for Python._ - [mocpy](https://github.com/cds-astro/mocpy) _Astronomical Python library offering data structures for describing any arbitrary coverage regions on the unit sphere._ +- [obstore](https://github.com/developmentseed/obstore) _The simplest, highest-throughput Python interface to Amazon S3, Google Cloud Storage, Azure Storage, & other S3-compliant APIs, powered by Rust._ - [opendal](https://github.com/apache/opendal/tree/main/bindings/python) _A data access layer that allows users to easily and efficiently retrieve data from various storage services in a unified way._ - [orjson](https://github.com/ijl/orjson) _Fast Python JSON library._ - [ormsgpack](https://github.com/aviramha/ormsgpack) _Fast Python msgpack library._ From a4c35cee3006d40c7c22fee50dc03125fafe82fc Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Fri, 6 Jun 2025 11:23:07 -0400 Subject: [PATCH 15/16] Make PyObjectObRefcnt.refcnt_and_flags 64-bit-only (#5180) * Make PyObjectObRefcnt.refcnt_and_flags 64-bit-only Fix #5175 as described in https://github.com/PyO3/pyo3/issues/5175#issuecomment-2939918138. * Add a news fragment for #5180 --- newsfragments/5180.fixed.md | 1 + pyo3-ffi/src/object.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 newsfragments/5180.fixed.md diff --git a/newsfragments/5180.fixed.md b/newsfragments/5180.fixed.md new file mode 100644 index 00000000000..9be577d65ab --- /dev/null +++ b/newsfragments/5180.fixed.md @@ -0,0 +1 @@ +Fixed segmentation faults on 32-bit ix86 with Python 3.14 diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index c4c5abc361c..6d5dc781287 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -49,7 +49,7 @@ pub struct PyObjectObFlagsAndRefcnt { pub union PyObjectObRefcnt { #[cfg(all(target_pointer_width = "64", Py_3_14))] pub ob_refcnt_full: crate::PY_INT64_T, - #[cfg(Py_3_14)] + #[cfg(all(target_pointer_width = "64", Py_3_14))] pub refcnt_and_flags: PyObjectObFlagsAndRefcnt, pub ob_refcnt: Py_ssize_t, #[cfg(all(target_pointer_width = "64", not(Py_3_14)))] From 2aacae6cef1bc806a642d3018bb66a307a7850bd Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 11 Jun 2025 13:58:30 +0100 Subject: [PATCH 16/16] release: 0.25.1 Co-authored-by: Alex Gaynor --- CHANGELOG.md | 25 ++++++++++++++++++- Cargo.toml | 8 +++--- README.md | 4 +-- examples/decorator/.template/pre-script.rhai | 2 +- .../maturin-starter/.template/pre-script.rhai | 2 +- examples/plugin/.template/pre-script.rhai | 2 +- .../.template/pre-script.rhai | 2 +- examples/word-count/.template/pre-script.rhai | 2 +- newsfragments/5121.added.md | 1 - newsfragments/5121.changed.md | 2 -- newsfragments/5145.packaging.md | 1 - newsfragments/5150.added.md | 1 - newsfragments/5156.fixed.md | 1 - newsfragments/5161.fixed.md | 1 - newsfragments/5174.added.md | 1 - newsfragments/5180.fixed.md | 1 - pyo3-build-config/Cargo.toml | 2 +- pyo3-ffi/Cargo.toml | 4 +-- pyo3-ffi/README.md | 4 +-- pyo3-introspection/Cargo.toml | 2 +- pyo3-macros-backend/Cargo.toml | 6 ++--- pyo3-macros/Cargo.toml | 4 +-- pyproject.toml | 2 +- tests/ui/reject_generics.stderr | 4 +-- 24 files changed, 49 insertions(+), 35 deletions(-) delete mode 100644 newsfragments/5121.added.md delete mode 100644 newsfragments/5121.changed.md delete mode 100644 newsfragments/5145.packaging.md delete mode 100644 newsfragments/5150.added.md delete mode 100644 newsfragments/5156.fixed.md delete mode 100644 newsfragments/5161.fixed.md delete mode 100644 newsfragments/5174.added.md delete mode 100644 newsfragments/5180.fixed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b13c92caef9..9d7bc1ac680 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,28 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.25.1] - 2025-06-12 +### Packaging + +- Add support for Windows on ARM64. [#5145](https://github.com/PyO3/pyo3/pull/5145) +- Add `chrono-local` feature for optional conversions for chrono's `Local` timezone & `DateTime` instances. [#5174](https://github.com/PyO3/pyo3/pull/5174) + +### Added + +- Add FFI definition `PyBytes_AS_STRING`. [#5121](https://github.com/PyO3/pyo3/pull/5121) +- Add support for module associated consts introspection. [#5150](https://github.com/PyO3/pyo3/pull/5150) + +### Changed + +- Enable "vectorcall" FFI definitions on GraalPy. [#5121](https://github.com/PyO3/pyo3/pull/5121) +- Use `Py_Is` function on GraalPy [#5121](https://github.com/PyO3/pyo3/pull/5121) + +### Fixed + +- Report a better compile error for `async` declarations when not using `experimental-async` feature. [#5156](https://github.com/PyO3/pyo3/pull/5156) +- Fix implementation of `FromPyObject` for `uuid::Uuid` on big-endian architectures. [#5161](https://github.com/PyO3/pyo3/pull/5161) +- Fix segmentation faults on 32-bit x86 with Python 3.14. [#5180](https://github.com/PyO3/pyo3/pull/5180) + ## [0.25.0] - 2025-05-14 ### Packaging @@ -2198,7 +2220,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.25.0...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.25.1...HEAD +[0.25.0]: https://github.com/pyo3/pyo3/compare/v0.25.0...v0.25.1 [0.25.0]: https://github.com/pyo3/pyo3/compare/v0.24.2...v0.25.0 [0.24.2]: https://github.com/pyo3/pyo3/compare/v0.24.1...v0.24.2 [0.24.1]: https://github.com/pyo3/pyo3/compare/v0.24.0...v0.24.1 diff --git a/Cargo.toml b/Cargo.toml index c4a97f0ab3a..0b9296090e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.25.0" +version = "0.25.1" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -20,10 +20,10 @@ memoffset = "0.9" once_cell = "1.13" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently -pyo3-ffi = { path = "pyo3-ffi", version = "=0.25.0" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.25.1" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.25.0", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.25.1", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -74,7 +74,7 @@ uuid = { version = "1.10.0", features = ["v4"] } parking_lot = { version = "0.12.3", features = ["arc_lock"]} [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "=0.25.0", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.25.1", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/README.md b/README.md index 31ed8b7e47a..578e05198f2 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.25.0", features = ["extension-module"] } +pyo3 = { version = "0.25.1", features = ["extension-module"] } ``` **`src/lib.rs`** @@ -140,7 +140,7 @@ Start a new project with `cargo new` and add `pyo3` to the `Cargo.toml` like th ```toml [dependencies.pyo3] -version = "0.25.0" +version = "0.25.1" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index 80ebe3ce006..110a6e82130 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.25.0"); +variable::set("PYO3_VERSION", "0.25.1"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/examples/maturin-starter/.template/pre-script.rhai b/examples/maturin-starter/.template/pre-script.rhai index 80ebe3ce006..110a6e82130 100644 --- a/examples/maturin-starter/.template/pre-script.rhai +++ b/examples/maturin-starter/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.25.0"); +variable::set("PYO3_VERSION", "0.25.1"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/examples/plugin/.template/pre-script.rhai b/examples/plugin/.template/pre-script.rhai index 2a21f6b2e53..d21dae34ad8 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.25.0"); +variable::set("PYO3_VERSION", "0.25.1"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/plugin_api/Cargo.toml", "plugin_api/Cargo.toml"); file::delete(".template"); diff --git a/examples/setuptools-rust-starter/.template/pre-script.rhai b/examples/setuptools-rust-starter/.template/pre-script.rhai index 92eeabb3ea8..b27db6f2700 100644 --- a/examples/setuptools-rust-starter/.template/pre-script.rhai +++ b/examples/setuptools-rust-starter/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.25.0"); +variable::set("PYO3_VERSION", "0.25.1"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/setup.cfg", "setup.cfg"); file::delete(".template"); diff --git a/examples/word-count/.template/pre-script.rhai b/examples/word-count/.template/pre-script.rhai index 80ebe3ce006..110a6e82130 100644 --- a/examples/word-count/.template/pre-script.rhai +++ b/examples/word-count/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.25.0"); +variable::set("PYO3_VERSION", "0.25.1"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/5121.added.md b/newsfragments/5121.added.md deleted file mode 100644 index fd79e5a8ea7..00000000000 --- a/newsfragments/5121.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `PyBytes_AS_STRING` diff --git a/newsfragments/5121.changed.md b/newsfragments/5121.changed.md deleted file mode 100644 index 49b317cb96b..00000000000 --- a/newsfragments/5121.changed.md +++ /dev/null @@ -1,2 +0,0 @@ -Enable vectorcall methods on GraalPy -Call Py_Is function on GraalPy diff --git a/newsfragments/5145.packaging.md b/newsfragments/5145.packaging.md deleted file mode 100644 index a11116acc28..00000000000 --- a/newsfragments/5145.packaging.md +++ /dev/null @@ -1 +0,0 @@ -add support for Windows on ARM64. \ No newline at end of file diff --git a/newsfragments/5150.added.md b/newsfragments/5150.added.md deleted file mode 100644 index dec859ce549..00000000000 --- a/newsfragments/5150.added.md +++ /dev/null @@ -1 +0,0 @@ -- Add support for module associated consts introspection. \ No newline at end of file diff --git a/newsfragments/5156.fixed.md b/newsfragments/5156.fixed.md deleted file mode 100644 index 2c6049ef855..00000000000 --- a/newsfragments/5156.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Catching `async` declaration when not using `experimental-async` \ No newline at end of file diff --git a/newsfragments/5161.fixed.md b/newsfragments/5161.fixed.md deleted file mode 100644 index 1a40c79bbe8..00000000000 --- a/newsfragments/5161.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fixed FromPyObject impl for `uuid::Uuid` on big-endian architectures diff --git a/newsfragments/5174.added.md b/newsfragments/5174.added.md deleted file mode 100644 index 09b7dec02e7..00000000000 --- a/newsfragments/5174.added.md +++ /dev/null @@ -1 +0,0 @@ -Add conversions for chrono's `Local` timezone & `DateTime` instances. diff --git a/newsfragments/5180.fixed.md b/newsfragments/5180.fixed.md deleted file mode 100644 index 9be577d65ab..00000000000 --- a/newsfragments/5180.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fixed segmentation faults on 32-bit ix86 with Python 3.14 diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 8070860a1e9..e7c603be867 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.25.0" +version = "0.25.1" description = "Build configuration for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] diff --git a/pyo3-ffi/Cargo.toml b/pyo3-ffi/Cargo.toml index 647b76fbdcd..1d2d0d55eda 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.25.0" +version = "0.25.1" description = "Python-API bindings for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -44,7 +44,7 @@ generate-import-lib = ["pyo3-build-config/python3-dll-a"] paste = "1" [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.1", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index 09be0a06f66..dc5e12827af 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -41,13 +41,13 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies.pyo3-ffi] -version = "0.25.0" +version = "0.25.1" features = ["extension-module"] [build-dependencies] # This is only necessary if you need to configure your build based on # the Python version or the compile-time configuration for the interpreter. -pyo3_build_config = "0.25.0" +pyo3_build_config = "0.25.1" ``` If you need to use conditional compilation based on Python version or how diff --git a/pyo3-introspection/Cargo.toml b/pyo3-introspection/Cargo.toml index 98dfdebbd10..5c507338471 100644 --- a/pyo3-introspection/Cargo.toml +++ b/pyo3-introspection/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-introspection" -version = "0.25.0" +version = "0.25.1" description = "Introspect dynamic libraries built with PyO3 to get metadata about the exported Python types" authors = ["PyO3 Project and Contributors "] homepage = "https://github.com/pyo3/pyo3" diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index d626fa1ebf7..5e8bce9337a 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.25.0" +version = "0.25.1" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -17,7 +17,7 @@ rust-version = "1.63" [dependencies] heck = "0.5" proc-macro2 = { version = "1.0.60", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.1", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] @@ -26,7 +26,7 @@ default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.0" } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.1" } [lints] workspace = true diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 7df15c23f76..d96e903fc9c 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.25.0" +version = "0.25.1" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -23,7 +23,7 @@ experimental-inspect = ["pyo3-macros-backend/experimental-inspect"] proc-macro2 = { version = "1.0.60", default-features = false } quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } -pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.25.0" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.25.1" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index adfe3a27348..48316a920a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.25.0" +version = "0.25.1" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}" diff --git a/tests/ui/reject_generics.stderr b/tests/ui/reject_generics.stderr index 9b16f1d68e7..02db8433838 100644 --- a/tests/ui/reject_generics.stderr +++ b/tests/ui/reject_generics.stderr @@ -1,10 +1,10 @@ -error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.25.0/class.html#no-generic-parameters +error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.25.1/class.html#no-generic-parameters --> tests/ui/reject_generics.rs:4:25 | 4 | struct ClassWithGenerics { | ^ -error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.25.0/class.html#no-lifetime-parameters +error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.25.1/class.html#no-lifetime-parameters --> tests/ui/reject_generics.rs:9:27 | 9 | struct ClassWithLifetimes<'a> { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy