diff --git a/Cargo.toml b/Cargo.toml index e54ac656..fe18ed7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ serde_json = "1.0.61" pyo3-build-config = { path = "pyo3-build-config", version = "0.15.1", features = ["resolve-config"] } [features] -default = ["macros"] +default = ["macros", "pyproto"] # Enables macros: #[pyclass], #[pymodule], #[pyfunction] etc. macros = ["pyo3-macros", "indoc", "unindent"] @@ -60,6 +60,9 @@ macros = ["pyo3-macros", "indoc", "unindent"] # Enables multiple #[pymethods] per #[pyclass] multiple-pymethods = ["inventory", "pyo3-macros/multiple-pymethods"] +# Enables deprecated #[pyproto] macro +pyproto = ["pyo3-macros/pyproto"] + # Use this feature when building an extension module. # It tells the linker to keep the python symbols unresolved, # so that the module can also be used with statically linked python interpreters. @@ -83,7 +86,7 @@ nightly = [] # Activates all additional features # This is mostly intended for testing purposes - activating *all* of these isn't particularly useful. -full = ["macros", "multiple-pymethods", "num-bigint", "num-complex", "hashbrown", "serde", "indexmap", "eyre", "anyhow"] +full = ["macros", "pyproto", "multiple-pymethods", "num-bigint", "num-complex", "hashbrown", "serde", "indexmap", "eyre", "anyhow"] [[bench]] name = "bench_call" diff --git a/Makefile b/Makefile index d7a87d47..668807e3 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,6 @@ ALL_ADDITIVE_FEATURES = macros multiple-pymethods num-bigint num-complex hashbrown serde indexmap eyre anyhow COVERAGE_PACKAGES = --package pyo3 --package pyo3-build-config --package pyo3-macros-backend --package pyo3-macros -list_all_additive_features: - @echo $(ALL_ADDITIVE_FEATURES) - test: lint test_py cargo test cargo test --features="abi3" diff --git a/guide/src/class.md b/guide/src/class.md index 51c6e01d..8ad79a35 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -849,17 +849,17 @@ impl pyo3::IntoPy for MyClass { } } -impl pyo3::class::impl_::PyClassImpl for MyClass { +impl pyo3::impl_::pyclass::PyClassImpl for MyClass { const DOC: &'static str = "Class for demonstration\u{0}"; const IS_GC: bool = false; const IS_BASETYPE: bool = false; const IS_SUBCLASS: bool = false; type Layout = PyCell; type BaseType = PyAny; - type ThreadChecker = pyo3::class::impl_::ThreadCheckerStub; + type ThreadChecker = pyo3::impl_::pyclass::ThreadCheckerStub; fn for_each_method_def(visitor: &mut dyn FnMut(&[pyo3::class::PyMethodDefType])) { - use pyo3::class::impl_::*; + use pyo3::impl_::pyclass::*; let collector = PyClassImplCollector::::new(); visitor(collector.py_methods()); visitor(collector.py_class_descriptors()); @@ -870,23 +870,23 @@ impl pyo3::class::impl_::PyClassImpl for MyClass { visitor(collector.number_protocol_methods()); } fn get_new() -> Option { - use pyo3::class::impl_::*; + use pyo3::impl_::pyclass::*; let collector = PyClassImplCollector::::new(); collector.new_impl() } fn get_alloc() -> Option { - use pyo3::class::impl_::*; + use pyo3::impl_::pyclass::*; let collector = PyClassImplCollector::::new(); collector.alloc_impl() } fn get_free() -> Option { - use pyo3::class::impl_::*; + use pyo3::impl_::pyclass::*; let collector = PyClassImplCollector::::new(); collector.free_impl() } fn for_each_proto_slot(visitor: &mut dyn FnMut(&[pyo3::ffi::PyType_Slot])) { // Implementation which uses dtolnay specialization to load all slots. - use pyo3::class::impl_::*; + use pyo3::impl_::pyclass::*; let collector = PyClassImplCollector::::new(); visitor(collector.object_protocol_slots()); visitor(collector.number_protocol_slots()); diff --git a/guide/src/features.md b/guide/src/features.md index 1a29885c..e96b486b 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -65,6 +65,12 @@ Most users should only need a single `#[pymethods]` per `#[pyclass]`. In additio See [the `#[pyclass]` implementation details](class.md#implementation-details) for more information. +### `pyproto` + +This feature enables the `#[pyproto]` macro, which is an alternative (older, soon-to-be-deprecated) to `#[pymethods]` for defining magic methods such as `__eq__`. + +> This feature is enabled by default. To disable it, set `default-features = false` for the `pyo3` entry in your Cargo.toml. + ### `nightly` The `nightly` feature needs the nightly Rust compiler. This allows PyO3 to use Rust's unstable specialization feature to apply the following optimizations: diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 5abd99af..f46a0848 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -22,3 +22,6 @@ pyo3-build-config = { path = "../pyo3-build-config", version = "0.15.1", feature version = "1" default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"] + +[features] +pyproto = [] diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index c6419427..fd6ec4a4 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -9,6 +9,7 @@ mod utils; mod attributes; +#[cfg(feature = "pyproto")] mod defs; mod deprecations; mod frompyobject; @@ -16,11 +17,13 @@ mod konst; mod method; mod module; mod params; +#[cfg(feature = "pyproto")] mod proto_method; mod pyclass; mod pyfunction; mod pyimpl; mod pymethod; +#[cfg(feature = "pyproto")] mod pyproto; mod wrap; @@ -29,6 +32,7 @@ pub use module::{process_functions_in_module, pymodule_impl, PyModuleOptions}; pub use pyclass::{build_py_class, build_py_enum, PyClassArgs}; pub use pyfunction::{build_py_function, PyFunctionOptions}; pub use pyimpl::{build_py_methods, PyClassMethodsType}; +#[cfg(feature = "pyproto")] pub use pyproto::build_py_proto; pub use utils::get_doc; pub use wrap::{wrap_pyfunction_impl, wrap_pymodule_impl, WrapPyFunctionArgs}; diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 930b005b..8d03d75c 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -566,23 +566,23 @@ impl<'a> FnSpec<'a> { let doc = &self.doc; match self.convention { CallingConvention::Noargs => quote! { - _pyo3::class::methods::PyMethodDef::noargs( + _pyo3::impl_::pymethods::PyMethodDef::noargs( #python_name, - _pyo3::class::methods::PyCFunction(#wrapper), + _pyo3::impl_::pymethods::PyCFunction(#wrapper), #doc, ) }, CallingConvention::Fastcall => quote! { - _pyo3::class::methods::PyMethodDef::fastcall_cfunction_with_keywords( + _pyo3::impl_::pymethods::PyMethodDef::fastcall_cfunction_with_keywords( #python_name, - _pyo3::class::methods::PyCFunctionFastWithKeywords(#wrapper), + _pyo3::impl_::pymethods::PyCFunctionFastWithKeywords(#wrapper), #doc, ) }, CallingConvention::Varargs => quote! { - _pyo3::class::methods::PyMethodDef::cfunction_with_keywords( + _pyo3::impl_::pymethods::PyMethodDef::cfunction_with_keywords( #python_name, - _pyo3::class::methods::PyCFunctionWithKeywords(#wrapper), + _pyo3::impl_::pymethods::PyCFunctionWithKeywords(#wrapper), #doc, ) }, diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 3a32f4be..462b8a3c 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -511,11 +511,11 @@ fn unit_variants_as_descriptors<'a>( .map(|var| gen_py_const(&cls_type, &variant_to_attribute(var))); quote! { - impl _pyo3::class::impl_::PyClassDescriptors<#cls> - for _pyo3::class::impl_::PyClassImplCollector<#cls> + impl _pyo3::impl_::pyclass::PyClassDescriptors<#cls> + for _pyo3::impl_::pyclass::PyClassImplCollector<#cls> { - fn py_class_descriptors(self) -> &'static [_pyo3::class::methods::PyMethodDefType] { - static METHODS: &[_pyo3::class::methods::PyMethodDefType] = &[#(#py_methods),*]; + fn py_class_descriptors(self) -> &'static [_pyo3::impl_::pymethods::PyMethodDefType] { + static METHODS: &[_pyo3::impl_::pymethods::PyMethodDefType] = &[#(#py_methods),*]; METHODS } } @@ -574,11 +574,11 @@ fn impl_descriptors( .collect::>()?; Ok(quote! { - impl _pyo3::class::impl_::PyClassDescriptors<#cls> - for _pyo3::class::impl_::PyClassImplCollector<#cls> + impl _pyo3::impl_::pyclass::PyClassDescriptors<#cls> + for _pyo3::impl_::pyclass::PyClassImplCollector<#cls> { - fn py_class_descriptors(self) -> &'static [_pyo3::class::methods::PyMethodDefType] { - static METHODS: &[_pyo3::class::methods::PyMethodDefType] = &[#(#py_methods),*]; + fn py_class_descriptors(self) -> &'static [_pyo3::impl_::pymethods::PyMethodDefType] { + static METHODS: &[_pyo3::impl_::pymethods::PyMethodDefType] = &[#(#py_methods),*]; METHODS } } @@ -676,7 +676,7 @@ impl<'a> PyClassImplsBuilder<'a> { }; let base_nativetype = if attr.has_extends { - quote! { ::BaseNativeType } + quote! { ::BaseNativeType } } else { quote! { _pyo3::PyAny } }; @@ -749,13 +749,13 @@ impl<'a> PyClassImplsBuilder<'a> { }; let thread_checker = if self.attr.has_unsendable { - quote! { _pyo3::class::impl_::ThreadCheckerImpl<#cls> } + quote! { _pyo3::impl_::pyclass::ThreadCheckerImpl<#cls> } } else if self.attr.has_extends { quote! { - _pyo3::class::impl_::ThreadCheckerInherited<#cls, <#cls as _pyo3::class::impl_::PyClassImpl>::BaseType> + _pyo3::impl_::pyclass::ThreadCheckerInherited<#cls, <#cls as _pyo3::impl_::pyclass::PyClassImpl>::BaseType> } } else { - quote! { _pyo3::class::impl_::ThreadCheckerStub<#cls> } + quote! { _pyo3::impl_::pyclass::ThreadCheckerStub<#cls> } }; let (for_each_py_method, methods_protos, inventory, inventory_class) = match self @@ -775,13 +775,13 @@ impl<'a> PyClassImplsBuilder<'a> { ); ( quote! { - for inventory in _pyo3::inventory::iter::<::Inventory>() { - visitor(_pyo3::class::impl_::PyClassInventory::methods(inventory)); + for inventory in _pyo3::inventory::iter::<::Inventory>() { + visitor(_pyo3::impl_::pyclass::PyClassInventory::methods(inventory)); } }, quote! { - for inventory in _pyo3::inventory::iter::<::Inventory>() { - visitor(_pyo3::class::impl_::PyClassInventory::slots(inventory)); + for inventory in _pyo3::inventory::iter::<::Inventory>() { + visitor(_pyo3::impl_::pyclass::PyClassInventory::slots(inventory)); } }, Some(quote! { type Inventory = #inventory_class_name; }), @@ -789,8 +789,37 @@ impl<'a> PyClassImplsBuilder<'a> { ) } }; + + let pyproto_method_visitors = if cfg!(feature = "pyproto") { + Some(quote! { + visitor(collector.object_protocol_methods()); + visitor(collector.async_protocol_methods()); + visitor(collector.descr_protocol_methods()); + visitor(collector.mapping_protocol_methods()); + visitor(collector.number_protocol_methods()); + }) + } else { + None + }; + + let pyproto_proto_visitors = if cfg!(feature = "pyproto") { + Some(quote! { + visitor(collector.object_protocol_slots()); + visitor(collector.number_protocol_slots()); + visitor(collector.iter_protocol_slots()); + visitor(collector.gc_protocol_slots()); + visitor(collector.descr_protocol_slots()); + visitor(collector.mapping_protocol_slots()); + visitor(collector.sequence_protocol_slots()); + visitor(collector.async_protocol_slots()); + visitor(collector.buffer_protocol_slots()); + }) + } else { + None + }; + quote! { - impl _pyo3::class::impl_::PyClassImpl for #cls { + impl _pyo3::impl_::pyclass::PyClassImpl for #cls { const DOC: &'static str = #doc; const IS_GC: bool = #is_gc; const IS_BASETYPE: bool = #is_basetype; @@ -802,48 +831,36 @@ impl<'a> PyClassImplsBuilder<'a> { #inventory fn for_each_method_def(visitor: &mut dyn ::std::ops::FnMut(&[_pyo3::class::PyMethodDefType])) { - use _pyo3::class::impl_::*; + use _pyo3::impl_::pyclass::*; let collector = PyClassImplCollector::::new(); #for_each_py_method; visitor(collector.py_class_descriptors()); - visitor(collector.object_protocol_methods()); - visitor(collector.async_protocol_methods()); - visitor(collector.descr_protocol_methods()); - visitor(collector.mapping_protocol_methods()); - visitor(collector.number_protocol_methods()); + #pyproto_method_visitors } fn get_new() -> ::std::option::Option<_pyo3::ffi::newfunc> { - use _pyo3::class::impl_::*; + use _pyo3::impl_::pyclass::*; let collector = PyClassImplCollector::::new(); collector.new_impl() } fn get_alloc() -> ::std::option::Option<_pyo3::ffi::allocfunc> { - use _pyo3::class::impl_::*; + use _pyo3::impl_::pyclass::*; let collector = PyClassImplCollector::::new(); collector.alloc_impl() } fn get_free() -> ::std::option::Option<_pyo3::ffi::freefunc> { - use _pyo3::class::impl_::*; + use _pyo3::impl_::pyclass::*; let collector = PyClassImplCollector::::new(); collector.free_impl() } fn for_each_proto_slot(visitor: &mut dyn ::std::ops::FnMut(&[_pyo3::ffi::PyType_Slot])) { // Implementation which uses dtolnay specialization to load all slots. - use _pyo3::class::impl_::*; + use _pyo3::impl_::pyclass::*; let collector = PyClassImplCollector::::new(); // This depends on Python implementation detail; // an old slot entry will be overriden by newer ones. visitor(collector.py_class_default_slots()); - visitor(collector.object_protocol_slots()); - visitor(collector.number_protocol_slots()); - visitor(collector.iter_protocol_slots()); - visitor(collector.gc_protocol_slots()); - visitor(collector.descr_protocol_slots()); - visitor(collector.mapping_protocol_slots()); - visitor(collector.sequence_protocol_slots()); - visitor(collector.async_protocol_slots()); - visitor(collector.buffer_protocol_slots()); + #pyproto_proto_visitors #methods_protos } @@ -861,7 +878,7 @@ impl<'a> PyClassImplsBuilder<'a> { self.attr.freelist.as_ref().map_or(quote!{}, |freelist| { quote! { - impl _pyo3::class::impl_::PyClassWithFreeList for #cls { + impl _pyo3::impl_::pyclass::PyClassWithFreeList for #cls { #[inline] fn get_free_list(_py: _pyo3::Python<'_>) -> &mut _pyo3::impl_::freelist::FreeList<*mut _pyo3::ffi::PyObject> { static mut FREELIST: *mut _pyo3::impl_::freelist::FreeList<*mut _pyo3::ffi::PyObject> = 0 as *mut _; @@ -875,17 +892,17 @@ impl<'a> PyClassImplsBuilder<'a> { } } - impl _pyo3::class::impl_::PyClassAllocImpl<#cls> for _pyo3::class::impl_::PyClassImplCollector<#cls> { + impl _pyo3::impl_::pyclass::PyClassAllocImpl<#cls> for _pyo3::impl_::pyclass::PyClassImplCollector<#cls> { #[inline] fn alloc_impl(self) -> ::std::option::Option<_pyo3::ffi::allocfunc> { - ::std::option::Option::Some(_pyo3::class::impl_::alloc_with_freelist::<#cls>) + ::std::option::Option::Some(_pyo3::impl_::pyclass::alloc_with_freelist::<#cls>) } } - impl _pyo3::class::impl_::PyClassFreeImpl<#cls> for _pyo3::class::impl_::PyClassImplCollector<#cls> { + impl _pyo3::impl_::pyclass::PyClassFreeImpl<#cls> for _pyo3::impl_::pyclass::PyClassImplCollector<#cls> { #[inline] fn free_impl(self) -> ::std::option::Option<_pyo3::ffi::freefunc> { - ::std::option::Option::Some(_pyo3::class::impl_::free_with_freelist::<#cls>) + ::std::option::Option::Some(_pyo3::impl_::pyclass::free_with_freelist::<#cls>) } } } @@ -928,7 +945,7 @@ fn define_inventory_class(inventory_class_name: &syn::Ident) -> TokenStream { } } - impl _pyo3::class::impl_::PyClassInventory for #inventory_class_name { + impl _pyo3::impl_::pyclass::PyClassInventory for #inventory_class_name { fn methods(&'static self) -> &'static [_pyo3::class::PyMethodDefType] { self.methods } diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index bb5a90f7..e2d0be19 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -190,7 +190,7 @@ pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec) -> TokenStream { _pyo3::class::PyMethodDefType::ClassAttribute({ _pyo3::class::PyClassAttributeDef::new( #python_name, - _pyo3::class::methods::PyClassAttributeFactory({ + _pyo3::impl_::pymethods::PyClassAttributeFactory({ fn __wrap(py: _pyo3::Python<'_>) -> _pyo3::PyObject { #deprecations _pyo3::IntoPy::into_py(#cls::#member, py) @@ -234,8 +234,8 @@ pub fn gen_default_slot_impls(cls: &syn::Ident, method_defs: Vec) - impl #cls { #(#method_defs)* } - impl ::pyo3::class::impl_::PyClassDefaultSlots<#cls> - for ::pyo3::class::impl_::PyClassImplCollector<#cls> { + impl ::pyo3::impl_::pyclass::PyClassDefaultSlots<#cls> + for ::pyo3::impl_::pyclass::PyClassImplCollector<#cls> { fn py_class_default_slots(self) -> &'static [::pyo3::ffi::PyType_Slot] { &[#(#proto_impls),*] } @@ -245,11 +245,11 @@ pub fn gen_default_slot_impls(cls: &syn::Ident, method_defs: Vec) - fn impl_py_methods(ty: &syn::Type, methods: Vec) -> TokenStream { quote! { - impl _pyo3::class::impl_::PyMethods<#ty> - for _pyo3::class::impl_::PyClassImplCollector<#ty> + impl _pyo3::impl_::pyclass::PyMethods<#ty> + for _pyo3::impl_::pyclass::PyClassImplCollector<#ty> { - fn py_methods(self) -> &'static [_pyo3::class::methods::PyMethodDefType] { - static METHODS: &[_pyo3::class::methods::PyMethodDefType] = &[#(#methods),*]; + fn py_methods(self) -> &'static [_pyo3::impl_::pymethods::PyMethodDefType] { + static METHODS: &[_pyo3::impl_::pymethods::PyMethodDefType] = &[#(#methods),*]; METHODS } } @@ -266,7 +266,7 @@ fn add_shared_proto_slots( let first_implemented = implemented_proto_fragments.remove($first); let second_implemented = implemented_proto_fragments.remove($second); if first_implemented || second_implemented { - proto_impls.push(quote! { _pyo3::class::impl_::$slot!(#ty) }) + proto_impls.push(quote! { _pyo3::impl_::pyclass::$slot!(#ty) }) } }}; } @@ -300,8 +300,8 @@ fn add_shared_proto_slots( fn impl_protos(ty: &syn::Type, proto_impls: Vec) -> TokenStream { quote! { - impl _pyo3::class::impl_::PyMethodsProtocolSlots<#ty> - for _pyo3::class::impl_::PyClassImplCollector<#ty> + impl _pyo3::impl_::pyclass::PyMethodsProtocolSlots<#ty> + for _pyo3::impl_::pyclass::PyClassImplCollector<#ty> { fn methods_protocol_slots(self) -> &'static [_pyo3::ffi::PyType_Slot] { &[#(#proto_impls),*] @@ -317,7 +317,7 @@ fn submit_methods_inventory( ) -> TokenStream { quote! { _pyo3::inventory::submit! { - type Inventory = <#ty as _pyo3::class::impl_::PyClassImpl>::Inventory; + type Inventory = <#ty as _pyo3::impl_::pyclass::PyClassImpl>::Inventory; Inventory::new(&[#(#methods),*], &[#(#proto_impls),*]) } } diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 57aac487..c4c901b8 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -194,7 +194,7 @@ fn impl_py_method_def_new(cls: &syn::Type, spec: &FnSpec) -> Result let wrapper_ident = syn::Ident::new("__wrap", Span::call_site()); let wrapper = spec.get_wrapper_function(&wrapper_ident, Some(cls))?; Ok(quote! { - impl _pyo3::class::impl_::PyClassNewImpl<#cls> for _pyo3::class::impl_::PyClassImplCollector<#cls> { + impl _pyo3::impl_::pyclass::PyClassNewImpl<#cls> for _pyo3::impl_::pyclass::PyClassImplCollector<#cls> { fn new_impl(self) -> ::std::option::Option<_pyo3::ffi::newfunc> { ::std::option::Option::Some({ #wrapper @@ -229,7 +229,7 @@ fn impl_py_class_attribute(cls: &syn::Type, spec: &FnSpec) -> TokenStream { _pyo3::class::PyMethodDefType::ClassAttribute({ _pyo3::class::PyClassAttributeDef::new( #python_name, - _pyo3::class::methods::PyClassAttributeFactory({ + _pyo3::impl_::pymethods::PyClassAttributeFactory({ fn __wrap(py: _pyo3::Python<'_>) -> _pyo3::PyObject { #deprecations _pyo3::IntoPy::into_py(#cls::#name(), py) @@ -299,7 +299,7 @@ pub fn impl_py_setter_def(cls: &syn::Type, property_type: PropertyType) -> Resul #deprecations _pyo3::class::PySetterDef::new( #python_name, - _pyo3::class::methods::PySetter({ + _pyo3::impl_::pymethods::PySetter({ unsafe extern "C" fn __wrap( _slf: *mut _pyo3::ffi::PyObject, _value: *mut _pyo3::ffi::PyObject, @@ -379,7 +379,7 @@ pub fn impl_py_getter_def(cls: &syn::Type, property_type: PropertyType) -> Resul #deprecations _pyo3::class::PyGetterDef::new( #python_name, - _pyo3::class::methods::PyGetter({ + _pyo3::impl_::pymethods::PyGetter({ unsafe extern "C" fn __wrap( _slf: *mut _pyo3::ffi::PyObject, _: *mut ::std::os::raw::c_void @@ -972,7 +972,7 @@ impl SlotFragmentDef { let body = generate_method_body(cls, spec, &py, arguments, *extract_error_mode, None)?; let ret_ty = ret_ty.ffi_type(); Ok(quote! { - impl _pyo3::class::impl_::#fragment_trait<#cls> for _pyo3::class::impl_::PyClassImplCollector<#cls> { + impl _pyo3::impl_::pyclass::#fragment_trait<#cls> for _pyo3::impl_::pyclass::PyClassImplCollector<#cls> { #[inline] unsafe fn #method( diff --git a/pyo3-macros-backend/src/pyproto.rs b/pyo3-macros-backend/src/pyproto.rs index 198fb517..3c7973ac 100644 --- a/pyo3-macros-backend/src/pyproto.rs +++ b/pyo3-macros-backend/src/pyproto.rs @@ -109,11 +109,11 @@ fn impl_normal_methods( let methods_trait = proto.methods_trait(); let methods_trait_methods = proto.methods_trait_methods(); quote! { - impl _pyo3::class::impl_::#methods_trait<#ty> - for _pyo3::class::impl_::PyClassImplCollector<#ty> + impl _pyo3::impl_::pyclass::#methods_trait<#ty> + for _pyo3::impl_::pyclass::PyClassImplCollector<#ty> { - fn #methods_trait_methods(self) -> &'static [_pyo3::class::methods::PyMethodDefType] { - static METHODS: &[_pyo3::class::methods::PyMethodDefType] = + fn #methods_trait_methods(self) -> &'static [_pyo3::impl_::pymethods::PyMethodDefType] { + static METHODS: &[_pyo3::impl_::pymethods::PyMethodDefType] = &[#(#py_methods),*]; METHODS } @@ -153,8 +153,8 @@ fn impl_proto_methods( } quote! { - impl _pyo3::class::impl_::#slots_trait<#ty> - for _pyo3::class::impl_::PyClassImplCollector<#ty> + impl _pyo3::impl_::pyclass::#slots_trait<#ty> + for _pyo3::impl_::pyclass::PyClassImplCollector<#ty> { fn #slots_trait_slots(self) -> &'static [_pyo3::ffi::PyType_Slot] { &[#(#tokens),*] diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 6f55d41a..a12d7d7c 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -16,6 +16,8 @@ proc-macro = true [features] multiple-pymethods = [] +pyproto = ["pyo3-macros-backend/pyproto"] + [dependencies] proc-macro2 = { version = "1", default-features = false } quote = "1" diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index c03d3c86..54070202 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -9,9 +9,8 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use pyo3_macros_backend::{ build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods, - build_py_proto, get_doc, process_functions_in_module, pymodule_impl, wrap_pyfunction_impl, - wrap_pymodule_impl, PyClassArgs, PyClassMethodsType, PyFunctionOptions, PyModuleOptions, - WrapPyFunctionArgs, + get_doc, process_functions_in_module, pymodule_impl, wrap_pyfunction_impl, wrap_pymodule_impl, + PyClassArgs, PyClassMethodsType, PyFunctionOptions, PyModuleOptions, WrapPyFunctionArgs, }; use quote::quote; use syn::{parse::Nothing, parse_macro_input}; @@ -68,9 +67,10 @@ pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { /// [4]: ../class/gc/trait.PyGCProtocol.html /// [5]: ../class/iter/trait.PyIterProtocol.html #[proc_macro_attribute] +#[cfg(feature = "pyproto")] pub fn pyproto(_: TokenStream, input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as syn::ItemImpl); - let expanded = build_py_proto(&mut ast).unwrap_or_compile_error(); + let expanded = pyo3_macros_backend::build_py_proto(&mut ast).unwrap_or_compile_error(); quote!( #ast diff --git a/pytests/src/subclassing.rs b/pytests/src/subclassing.rs index d54bbc4d..8fd111e0 100644 --- a/pytests/src/subclassing.rs +++ b/pytests/src/subclassing.rs @@ -11,10 +11,7 @@ impl Subclassable { fn new() -> Self { Subclassable {} } -} -#[pyproto] -impl pyo3::PyObjectProtocol for Subclassable { fn __str__(&self) -> PyResult<&'static str> { Ok("Subclassable") } diff --git a/src/class/basic.rs b/src/class/basic.rs index 9d4d569b..b2d0e9ab 100644 --- a/src/class/basic.rs +++ b/src/class/basic.rs @@ -12,37 +12,6 @@ use crate::callback::{HashCallbackOutput, IntoPyCallbackOutput}; use crate::{exceptions, ffi, FromPyObject, PyAny, PyCell, PyClass, PyObject}; use std::os::raw::c_int; -/// Operators for the `__richcmp__` method -#[derive(Debug, Clone, Copy)] -pub enum CompareOp { - /// The *less than* operator. - Lt = ffi::Py_LT as isize, - /// The *less than or equal to* operator. - Le = ffi::Py_LE as isize, - /// The equality operator. - Eq = ffi::Py_EQ as isize, - /// The *not equal to* operator. - Ne = ffi::Py_NE as isize, - /// The *greater than* operator. - Gt = ffi::Py_GT as isize, - /// The *greater than or equal to* operator. - Ge = ffi::Py_GE as isize, -} - -impl CompareOp { - pub fn from_raw(op: c_int) -> Option { - match op { - ffi::Py_LT => Some(CompareOp::Lt), - ffi::Py_LE => Some(CompareOp::Le), - ffi::Py_EQ => Some(CompareOp::Eq), - ffi::Py_NE => Some(CompareOp::Ne), - ffi::Py_GT => Some(CompareOp::Gt), - ffi::Py_GE => Some(CompareOp::Ge), - _ => None, - } - } -} - /// Basic Python class customization #[allow(unused_variables)] pub trait PyObjectProtocol<'p>: PyClass { @@ -202,3 +171,5 @@ py_func_set_del!( __delattr__ ); py_unary_func!(bool, PyObjectBoolProtocol, T::__bool__, c_int); + +pub use crate::pyclass::CompareOp; diff --git a/src/class/impl_.rs b/src/class/impl_.rs deleted file mode 100644 index a3041da9..00000000 --- a/src/class/impl_.rs +++ /dev/null @@ -1,818 +0,0 @@ -// Copyright (c) 2017-present PyO3 Project and Contributors - -use crate::{ - exceptions::{PyAttributeError, PyNotImplementedError}, - ffi, - impl_::freelist::FreeList, - pycell::PyCellLayout, - pyclass_init::PyObjectInit, - type_object::{PyLayout, PyTypeObject}, - PyClass, PyMethodDefType, PyNativeType, PyResult, PyTypeInfo, Python, -}; -use std::{ - marker::PhantomData, - os::raw::{c_int, c_void}, - ptr::NonNull, - thread, -}; - -/// This type is used as a "dummy" type on which dtolnay specializations are -/// applied to apply implementations from `#[pymethods]` & `#[pyproto]` -pub struct PyClassImplCollector(PhantomData); - -impl PyClassImplCollector { - pub fn new() -> Self { - Self(PhantomData) - } -} - -impl Default for PyClassImplCollector { - fn default() -> Self { - Self::new() - } -} - -impl Clone for PyClassImplCollector { - fn clone(&self) -> Self { - Self::new() - } -} - -impl Copy for PyClassImplCollector {} - -/// Implements the underlying functionality of `#[pyclass]`, assembled by various proc macros. -/// -/// Users are discouraged from implementing this trait manually; it is a PyO3 implementation detail -/// and may be changed at any time. -pub trait PyClassImpl: Sized { - /// Class doc string - const DOC: &'static str = "\0"; - - /// #[pyclass(gc)] - const IS_GC: bool = false; - - /// #[pyclass(subclass)] - const IS_BASETYPE: bool = false; - - /// #[pyclass(extends=...)] - const IS_SUBCLASS: bool = false; - - /// Layout - type Layout: PyLayout; - - /// Base class - type BaseType: PyTypeInfo + PyTypeObject + PyClassBaseType; - - /// This handles following two situations: - /// 1. In case `T` is `Send`, stub `ThreadChecker` is used and does nothing. - /// This implementation is used by default. Compile fails if `T: !Send`. - /// 2. In case `T` is `!Send`, `ThreadChecker` panics when `T` is accessed by another thread. - /// This implementation is used when `#[pyclass(unsendable)]` is given. - /// Panicking makes it safe to expose `T: !Send` to the Python interpreter, where all objects - /// can be accessed by multiple threads by `threading` module. - type ThreadChecker: PyClassThreadChecker; - - #[cfg(feature = "multiple-pymethods")] - type Inventory: PyClassInventory; - - fn for_each_method_def(_visitor: &mut dyn FnMut(&[PyMethodDefType])) {} - fn for_each_proto_slot(_visitor: &mut dyn FnMut(&[ffi::PyType_Slot])) {} - - #[inline] - fn get_new() -> Option { - None - } - #[inline] - fn get_alloc() -> Option { - None - } - #[inline] - fn get_free() -> Option { - None - } - #[inline] - fn dict_offset() -> Option { - None - } - #[inline] - fn weaklist_offset() -> Option { - None - } -} - -// Traits describing known special methods. - -pub trait PyClassNewImpl { - fn new_impl(self) -> Option; -} - -impl PyClassNewImpl for &'_ PyClassImplCollector { - fn new_impl(self) -> Option { - None - } -} - -macro_rules! slot_fragment_trait { - ($trait_name:ident, $($default_method:tt)*) => { - #[allow(non_camel_case_types)] - pub trait $trait_name: Sized { - $($default_method)* - } - - impl $trait_name for &'_ PyClassImplCollector {} - } -} - -/// Macro which expands to three items -/// - Trait for a __setitem__ dunder -/// - Trait for the corresponding __delitem__ dunder -/// - A macro which will use dtolnay specialisation to generate the shared slot for the two dunders -macro_rules! define_pyclass_setattr_slot { - ( - $set_trait:ident, - $del_trait:ident, - $set:ident, - $del:ident, - $set_error:expr, - $del_error:expr, - $generate_macro:ident, - $slot:ident, - $func_ty:ident, - ) => { - slot_fragment_trait! { - $set_trait, - - /// # Safety: _slf and _attr must be valid non-null Python objects - #[inline] - unsafe fn $set( - self, - _py: Python, - _slf: *mut ffi::PyObject, - _attr: *mut ffi::PyObject, - _value: NonNull, - ) -> PyResult<()> { - $set_error - } - } - - slot_fragment_trait! { - $del_trait, - - /// # Safety: _slf and _attr must be valid non-null Python objects - #[inline] - unsafe fn $del( - self, - _py: Python, - _slf: *mut ffi::PyObject, - _attr: *mut ffi::PyObject, - ) -> PyResult<()> { - $del_error - } - } - - #[doc(hidden)] - #[macro_export] - macro_rules! $generate_macro { - ($cls:ty) => {{ - unsafe extern "C" fn __wrap( - _slf: *mut $crate::ffi::PyObject, - attr: *mut $crate::ffi::PyObject, - value: *mut $crate::ffi::PyObject, - ) -> ::std::os::raw::c_int { - use ::std::option::Option::*; - use $crate::callback::IntoPyCallbackOutput; - use $crate::class::impl_::*; - $crate::callback::handle_panic(|py| { - let collector = PyClassImplCollector::<$cls>::new(); - if let Some(value) = ::std::ptr::NonNull::new(value) { - collector.$set(py, _slf, attr, value).convert(py) - } else { - collector.$del(py, _slf, attr).convert(py) - } - }) - } - $crate::ffi::PyType_Slot { - slot: $crate::ffi::$slot, - pfunc: __wrap as $crate::ffi::$func_ty as _, - } - }}; - } - pub use $generate_macro; - }; -} - -define_pyclass_setattr_slot! { - PyClass__setattr__SlotFragment, - PyClass__delattr__SlotFragment, - __setattr__, - __delattr__, - Err(PyAttributeError::new_err("can't set attribute")), - Err(PyAttributeError::new_err("can't delete attribute")), - generate_pyclass_setattr_slot, - Py_tp_setattro, - setattrofunc, -} - -define_pyclass_setattr_slot! { - PyClass__set__SlotFragment, - PyClass__delete__SlotFragment, - __set__, - __delete__, - Err(PyNotImplementedError::new_err("can't set descriptor")), - Err(PyNotImplementedError::new_err("can't delete descriptor")), - generate_pyclass_setdescr_slot, - Py_tp_descr_set, - descrsetfunc, -} - -define_pyclass_setattr_slot! { - PyClass__setitem__SlotFragment, - PyClass__delitem__SlotFragment, - __setitem__, - __delitem__, - Err(PyNotImplementedError::new_err("can't set item")), - Err(PyNotImplementedError::new_err("can't delete item")), - generate_pyclass_setitem_slot, - Py_mp_ass_subscript, - objobjargproc, -} - -/// Macro which expands to three items -/// - Trait for a lhs dunder e.g. __add__ -/// - Trait for the corresponding rhs e.g. __radd__ -/// - A macro which will use dtolnay specialisation to generate the shared slot for the two dunders -macro_rules! define_pyclass_binary_operator_slot { - ( - $lhs_trait:ident, - $rhs_trait:ident, - $lhs:ident, - $rhs:ident, - $generate_macro:ident, - $slot:ident, - $func_ty:ident, - ) => { - slot_fragment_trait! { - $lhs_trait, - - /// # Safety: _slf and _other must be valid non-null Python objects - #[inline] - unsafe fn $lhs( - self, - _py: Python, - _slf: *mut ffi::PyObject, - _other: *mut ffi::PyObject, - ) -> PyResult<*mut ffi::PyObject> { - Ok(ffi::_Py_NewRef(ffi::Py_NotImplemented())) - } - } - - slot_fragment_trait! { - $rhs_trait, - - /// # Safety: _slf and _other must be valid non-null Python objects - #[inline] - unsafe fn $rhs( - self, - _py: Python, - _slf: *mut ffi::PyObject, - _other: *mut ffi::PyObject, - ) -> PyResult<*mut ffi::PyObject> { - Ok(ffi::_Py_NewRef(ffi::Py_NotImplemented())) - } - } - - #[doc(hidden)] - #[macro_export] - macro_rules! $generate_macro { - ($cls:ty) => {{ - unsafe extern "C" fn __wrap( - _slf: *mut $crate::ffi::PyObject, - _other: *mut $crate::ffi::PyObject, - ) -> *mut $crate::ffi::PyObject { - $crate::callback::handle_panic(|py| { - use $crate::class::impl_::*; - let collector = PyClassImplCollector::<$cls>::new(); - let lhs_result = collector.$lhs(py, _slf, _other)?; - if lhs_result == $crate::ffi::Py_NotImplemented() { - $crate::ffi::Py_DECREF(lhs_result); - collector.$rhs(py, _other, _slf) - } else { - ::std::result::Result::Ok(lhs_result) - } - }) - } - $crate::ffi::PyType_Slot { - slot: $crate::ffi::$slot, - pfunc: __wrap as $crate::ffi::$func_ty as _, - } - }}; - } - pub use $generate_macro; - }; -} - -define_pyclass_binary_operator_slot! { - PyClass__add__SlotFragment, - PyClass__radd__SlotFragment, - __add__, - __radd__, - generate_pyclass_add_slot, - Py_nb_add, - binaryfunc, -} - -define_pyclass_binary_operator_slot! { - PyClass__sub__SlotFragment, - PyClass__rsub__SlotFragment, - __sub__, - __rsub__, - generate_pyclass_sub_slot, - Py_nb_subtract, - binaryfunc, -} - -define_pyclass_binary_operator_slot! { - PyClass__mul__SlotFragment, - PyClass__rmul__SlotFragment, - __mul__, - __rmul__, - generate_pyclass_mul_slot, - Py_nb_multiply, - binaryfunc, -} - -define_pyclass_binary_operator_slot! { - PyClass__mod__SlotFragment, - PyClass__rmod__SlotFragment, - __mod__, - __rmod__, - generate_pyclass_mod_slot, - Py_nb_remainder, - binaryfunc, -} - -define_pyclass_binary_operator_slot! { - PyClass__divmod__SlotFragment, - PyClass__rdivmod__SlotFragment, - __divmod__, - __rdivmod__, - generate_pyclass_divmod_slot, - Py_nb_divmod, - binaryfunc, -} - -define_pyclass_binary_operator_slot! { - PyClass__lshift__SlotFragment, - PyClass__rlshift__SlotFragment, - __lshift__, - __rlshift__, - generate_pyclass_lshift_slot, - Py_nb_lshift, - binaryfunc, -} - -define_pyclass_binary_operator_slot! { - PyClass__rshift__SlotFragment, - PyClass__rrshift__SlotFragment, - __rshift__, - __rrshift__, - generate_pyclass_rshift_slot, - Py_nb_rshift, - binaryfunc, -} - -define_pyclass_binary_operator_slot! { - PyClass__and__SlotFragment, - PyClass__rand__SlotFragment, - __and__, - __rand__, - generate_pyclass_and_slot, - Py_nb_and, - binaryfunc, -} - -define_pyclass_binary_operator_slot! { - PyClass__or__SlotFragment, - PyClass__ror__SlotFragment, - __or__, - __ror__, - generate_pyclass_or_slot, - Py_nb_or, - binaryfunc, -} - -define_pyclass_binary_operator_slot! { - PyClass__xor__SlotFragment, - PyClass__rxor__SlotFragment, - __xor__, - __rxor__, - generate_pyclass_xor_slot, - Py_nb_xor, - binaryfunc, -} - -define_pyclass_binary_operator_slot! { - PyClass__matmul__SlotFragment, - PyClass__rmatmul__SlotFragment, - __matmul__, - __rmatmul__, - generate_pyclass_matmul_slot, - Py_nb_matrix_multiply, - binaryfunc, -} - -define_pyclass_binary_operator_slot! { - PyClass__truediv__SlotFragment, - PyClass__rtruediv__SlotFragment, - __truediv__, - __rtruediv__, - generate_pyclass_truediv_slot, - Py_nb_true_divide, - binaryfunc, -} - -define_pyclass_binary_operator_slot! { - PyClass__floordiv__SlotFragment, - PyClass__rfloordiv__SlotFragment, - __floordiv__, - __rfloordiv__, - generate_pyclass_floordiv_slot, - Py_nb_floor_divide, - binaryfunc, -} - -slot_fragment_trait! { - PyClass__pow__SlotFragment, - - /// # Safety: _slf and _other must be valid non-null Python objects - #[inline] - unsafe fn __pow__( - self, - _py: Python, - _slf: *mut ffi::PyObject, - _other: *mut ffi::PyObject, - _mod: *mut ffi::PyObject, - ) -> PyResult<*mut ffi::PyObject> { - Ok(ffi::_Py_NewRef(ffi::Py_NotImplemented())) - } -} - -slot_fragment_trait! { - PyClass__rpow__SlotFragment, - - /// # Safety: _slf and _other must be valid non-null Python objects - #[inline] - unsafe fn __rpow__( - self, - _py: Python, - _slf: *mut ffi::PyObject, - _other: *mut ffi::PyObject, - _mod: *mut ffi::PyObject, - ) -> PyResult<*mut ffi::PyObject> { - Ok(ffi::_Py_NewRef(ffi::Py_NotImplemented())) - } -} - -#[doc(hidden)] -#[macro_export] -macro_rules! generate_pyclass_pow_slot { - ($cls:ty) => {{ - unsafe extern "C" fn __wrap( - _slf: *mut $crate::ffi::PyObject, - _other: *mut $crate::ffi::PyObject, - _mod: *mut $crate::ffi::PyObject, - ) -> *mut $crate::ffi::PyObject { - $crate::callback::handle_panic(|py| { - use $crate::class::impl_::*; - let collector = PyClassImplCollector::<$cls>::new(); - let lhs_result = collector.__pow__(py, _slf, _other, _mod)?; - if lhs_result == $crate::ffi::Py_NotImplemented() { - $crate::ffi::Py_DECREF(lhs_result); - collector.__rpow__(py, _other, _slf, _mod) - } else { - ::std::result::Result::Ok(lhs_result) - } - }) - } - $crate::ffi::PyType_Slot { - slot: $crate::ffi::Py_nb_power, - pfunc: __wrap as $crate::ffi::ternaryfunc as _, - } - }}; -} -pub use generate_pyclass_pow_slot; - -pub trait PyClassAllocImpl { - fn alloc_impl(self) -> Option; -} - -impl PyClassAllocImpl for &'_ PyClassImplCollector { - fn alloc_impl(self) -> Option { - None - } -} - -pub trait PyClassFreeImpl { - fn free_impl(self) -> Option; -} - -impl PyClassFreeImpl for &'_ PyClassImplCollector { - fn free_impl(self) -> Option { - None - } -} - -/// Implements a freelist. -/// -/// Do not implement this trait manually. Instead, use `#[pyclass(freelist = N)]` -/// on a Rust struct to implement it. -pub trait PyClassWithFreeList: PyClass { - fn get_free_list(py: Python) -> &mut FreeList<*mut ffi::PyObject>; -} - -/// Implementation of tp_alloc for `freelist` classes. -/// -/// # Safety -/// - `subtype` must be a valid pointer to the type object of T or a subclass. -/// - The GIL must be held. -pub unsafe extern "C" fn alloc_with_freelist( - subtype: *mut ffi::PyTypeObject, - nitems: ffi::Py_ssize_t, -) -> *mut ffi::PyObject { - let py = Python::assume_gil_acquired(); - - #[cfg(not(Py_3_8))] - bpo_35810_workaround(py, subtype); - - let self_type = T::type_object_raw(py); - // If this type is a variable type or the subtype is not equal to this type, we cannot use the - // freelist - if nitems == 0 && subtype == self_type { - if let Some(obj) = T::get_free_list(py).pop() { - ffi::PyObject_Init(obj, subtype); - return obj as _; - } - } - - ffi::PyType_GenericAlloc(subtype, nitems) -} - -/// Implementation of tp_free for `freelist` classes. -/// -/// # Safety -/// - `obj` must be a valid pointer to an instance of T (not a subclass). -/// - The GIL must be held. -pub unsafe extern "C" fn free_with_freelist(obj: *mut c_void) { - let obj = obj as *mut ffi::PyObject; - debug_assert_eq!( - T::type_object_raw(Python::assume_gil_acquired()), - ffi::Py_TYPE(obj) - ); - if let Some(obj) = T::get_free_list(Python::assume_gil_acquired()).insert(obj) { - let ty = ffi::Py_TYPE(obj); - - // Deduce appropriate inverse of PyType_GenericAlloc - let free = if ffi::PyType_IS_GC(ty) != 0 { - ffi::PyObject_GC_Del - } else { - ffi::PyObject_Free - }; - free(obj as *mut c_void); - - #[cfg(Py_3_8)] - if ffi::PyType_HasFeature(ty, ffi::Py_TPFLAGS_HEAPTYPE) != 0 { - ffi::Py_DECREF(ty as *mut ffi::PyObject); - } - } -} - -/// Workaround for Python issue 35810; no longer necessary in Python 3.8 -#[inline] -#[cfg(not(Py_3_8))] -unsafe fn bpo_35810_workaround(_py: Python, ty: *mut ffi::PyTypeObject) { - #[cfg(Py_LIMITED_API)] - { - // Must check version at runtime for abi3 wheels - they could run against a higher version - // than the build config suggests. - use crate::once_cell::GILOnceCell; - static IS_PYTHON_3_8: GILOnceCell = GILOnceCell::new(); - - if *IS_PYTHON_3_8.get_or_init(_py, || _py.version_info() >= (3, 8)) { - // No fix needed - the wheel is running on a sufficiently new interpreter. - return; - } - } - - ffi::Py_INCREF(ty as *mut ffi::PyObject); -} - -// General methods implementation: either dtolnay specialization trait or inventory if -// multiple-pymethods feature is enabled. - -macro_rules! methods_trait { - ($name:ident, $function_name: ident) => { - pub trait $name { - fn $function_name(self) -> &'static [PyMethodDefType]; - } - - impl $name for &'_ PyClassImplCollector { - fn $function_name(self) -> &'static [PyMethodDefType] { - &[] - } - } - }; -} - -/// Implementation detail. Only to be used through our proc macro code. -/// Method storage for `#[pyclass]`. -/// Allows arbitrary `#[pymethod]` blocks to submit their methods, -/// which are eventually collected by `#[pyclass]`. -#[cfg(feature = "multiple-pymethods")] -pub trait PyClassInventory: inventory::Collect { - /// Returns the methods for a single `#[pymethods] impl` block - fn methods(&'static self) -> &'static [PyMethodDefType]; - - /// Returns the slots for a single `#[pymethods] impl` block - fn slots(&'static self) -> &'static [ffi::PyType_Slot]; -} - -// Methods from #[pyo3(get, set)] on struct fields. -methods_trait!(PyClassDescriptors, py_class_descriptors); - -// Methods from #[pymethods] if not using inventory. -#[cfg(not(feature = "multiple-pymethods"))] -methods_trait!(PyMethods, py_methods); - -// All traits describing slots, as well as the fallback implementations for unimplemented protos -// -// Protos which are implemented use dtolnay specialization to implement for PyClassImplCollector. -// -// See https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md - -macro_rules! slots_trait { - ($name:ident, $function_name: ident) => { - pub trait $name { - fn $function_name(self) -> &'static [ffi::PyType_Slot]; - } - - impl $name for &'_ PyClassImplCollector { - fn $function_name(self) -> &'static [ffi::PyType_Slot] { - &[] - } - } - }; -} - -slots_trait!(PyObjectProtocolSlots, object_protocol_slots); -slots_trait!(PyDescrProtocolSlots, descr_protocol_slots); -slots_trait!(PyGCProtocolSlots, gc_protocol_slots); -slots_trait!(PyIterProtocolSlots, iter_protocol_slots); -slots_trait!(PyMappingProtocolSlots, mapping_protocol_slots); -slots_trait!(PyNumberProtocolSlots, number_protocol_slots); -slots_trait!(PyAsyncProtocolSlots, async_protocol_slots); -slots_trait!(PySequenceProtocolSlots, sequence_protocol_slots); -slots_trait!(PyBufferProtocolSlots, buffer_protocol_slots); - -// slots that PyO3 implements by default, but can be overidden by the users. -slots_trait!(PyClassDefaultSlots, py_class_default_slots); - -// Protocol slots from #[pymethods] if not using inventory. -#[cfg(not(feature = "multiple-pymethods"))] -slots_trait!(PyMethodsProtocolSlots, methods_protocol_slots); - -methods_trait!(PyObjectProtocolMethods, object_protocol_methods); -methods_trait!(PyAsyncProtocolMethods, async_protocol_methods); -methods_trait!(PyDescrProtocolMethods, descr_protocol_methods); -methods_trait!(PyMappingProtocolMethods, mapping_protocol_methods); -methods_trait!(PyNumberProtocolMethods, number_protocol_methods); - -// Thread checkers - -#[doc(hidden)] -pub trait PyClassThreadChecker: Sized { - fn ensure(&self); - fn new() -> Self; - private_decl! {} -} - -/// Stub checker for `Send` types. -#[doc(hidden)] -pub struct ThreadCheckerStub(PhantomData); - -impl PyClassThreadChecker for ThreadCheckerStub { - fn ensure(&self) {} - #[inline] - fn new() -> Self { - ThreadCheckerStub(PhantomData) - } - private_impl! {} -} - -impl PyClassThreadChecker for ThreadCheckerStub { - fn ensure(&self) {} - #[inline] - fn new() -> Self { - ThreadCheckerStub(PhantomData) - } - private_impl! {} -} - -/// Thread checker for unsendable types. -/// Panics when the value is accessed by another thread. -#[doc(hidden)] -pub struct ThreadCheckerImpl(thread::ThreadId, PhantomData); - -impl PyClassThreadChecker for ThreadCheckerImpl { - fn ensure(&self) { - if thread::current().id() != self.0 { - panic!( - "{} is unsendable, but sent to another thread!", - std::any::type_name::() - ); - } - } - fn new() -> Self { - ThreadCheckerImpl(thread::current().id(), PhantomData) - } - private_impl! {} -} - -/// Thread checker for types that have `Send` and `extends=...`. -/// Ensures that `T: Send` and the parent is not accessed by another thread. -#[doc(hidden)] -pub struct ThreadCheckerInherited(PhantomData, U::ThreadChecker); - -impl PyClassThreadChecker for ThreadCheckerInherited { - fn ensure(&self) { - self.1.ensure(); - } - fn new() -> Self { - ThreadCheckerInherited(PhantomData, U::ThreadChecker::new()) - } - private_impl! {} -} - -/// Trait denoting that this class is suitable to be used as a base type for PyClass. -pub trait PyClassBaseType: Sized { - type LayoutAsBase: PyCellLayout; - type BaseNativeType; - type ThreadChecker: PyClassThreadChecker; - type Initializer: PyObjectInit; -} - -/// All PyClasses can be used as a base type. -impl PyClassBaseType for T { - type LayoutAsBase = crate::pycell::PyCell; - type BaseNativeType = T::BaseNativeType; - type ThreadChecker = T::ThreadChecker; - type Initializer = crate::pyclass_init::PyClassInitializer; -} - -/// Default new implementation -pub(crate) unsafe extern "C" fn fallback_new( - _subtype: *mut ffi::PyTypeObject, - _args: *mut ffi::PyObject, - _kwds: *mut ffi::PyObject, -) -> *mut ffi::PyObject { - crate::callback_body!(py, { - Err::<(), _>(crate::exceptions::PyTypeError::new_err( - "No constructor defined", - )) - }) -} - -/// Implementation of tp_dealloc for all pyclasses -pub(crate) unsafe extern "C" fn tp_dealloc(obj: *mut ffi::PyObject) { - crate::callback_body!(py, T::Layout::tp_dealloc(obj, py)) -} - -pub(crate) unsafe extern "C" fn get_sequence_item_from_mapping( - obj: *mut ffi::PyObject, - index: ffi::Py_ssize_t, -) -> *mut ffi::PyObject { - let index = ffi::PyLong_FromSsize_t(index); - if index.is_null() { - return std::ptr::null_mut(); - } - let result = ffi::PyObject_GetItem(obj, index); - ffi::Py_DECREF(index); - result -} - -pub(crate) unsafe extern "C" fn assign_sequence_item_from_mapping( - obj: *mut ffi::PyObject, - index: ffi::Py_ssize_t, - value: *mut ffi::PyObject, -) -> c_int { - let index = ffi::PyLong_FromSsize_t(index); - if index.is_null() { - return -1; - } - let result = if value.is_null() { - ffi::PyObject_DelItem(obj, index) - } else { - ffi::PyObject_SetItem(obj, index, value) - }; - ffi::Py_DECREF(index); - result -} diff --git a/src/class/iter.rs b/src/class/iter.rs index 2ac29a9d..3d980375 100644 --- a/src/class/iter.rs +++ b/src/class/iter.rs @@ -4,8 +4,7 @@ use crate::callback::IntoPyCallbackOutput; use crate::derive_utils::TryFromPyCell; -use crate::err::PyResult; -use crate::{ffi, IntoPy, IntoPyPointer, PyClass, PyObject, Python}; +use crate::{PyClass, PyObject}; /// Python Iterator Interface. /// @@ -73,49 +72,4 @@ pub trait PyIterNextProtocol<'p>: PyIterProtocol<'p> { py_unarys_func!(iter, PyIterIterProtocol, Self::__iter__); py_unarys_func!(iternext, PyIterNextProtocol, Self::__next__); -/// Output of `__next__` which can either `yield` the next value in the iteration, or -/// `return` a value to raise `StopIteration` in Python. -/// -/// See [`PyIterProtocol`](trait.PyIterProtocol.html) for an example. -pub enum IterNextOutput { - /// The value yielded by the iterator. - Yield(T), - /// The `StopIteration` object. - Return(U), -} - -pub type PyIterNextOutput = IterNextOutput; - -impl IntoPyCallbackOutput<*mut ffi::PyObject> for PyIterNextOutput { - fn convert(self, _py: Python) -> PyResult<*mut ffi::PyObject> { - match self { - IterNextOutput::Yield(o) => Ok(o.into_ptr()), - IterNextOutput::Return(opt) => Err(crate::exceptions::PyStopIteration::new_err((opt,))), - } - } -} - -impl IntoPyCallbackOutput for IterNextOutput -where - T: IntoPy, - U: IntoPy, -{ - fn convert(self, py: Python) -> PyResult { - match self { - IterNextOutput::Yield(o) => Ok(IterNextOutput::Yield(o.into_py(py))), - IterNextOutput::Return(o) => Ok(IterNextOutput::Return(o.into_py(py))), - } - } -} - -impl IntoPyCallbackOutput for Option -where - T: IntoPy, -{ - fn convert(self, py: Python) -> PyResult { - match self { - Some(o) => Ok(PyIterNextOutput::Yield(o.into_py(py))), - None => Ok(PyIterNextOutput::Return(py.None())), - } - } -} +pub use crate::pyclass::{IterNextOutput, PyIterNextOutput}; diff --git a/src/class/methods.rs b/src/class/methods.rs index 5f537e3a..e69de29b 100644 --- a/src/class/methods.rs +++ b/src/class/methods.rs @@ -1,220 +0,0 @@ -// Copyright (c) 2017-present PyO3 Project and Contributors - -use crate::internal_tricks::{extract_cstr_or_leak_cstring, NulByteInString}; -use crate::{ffi, PyObject, Python}; -use std::ffi::CStr; -use std::fmt; -use std::os::raw::c_int; - -/// `PyMethodDefType` represents different types of Python callable objects. -/// It is used by the `#[pymethods]` and `#[pyproto]` annotations. -#[derive(Debug)] -pub enum PyMethodDefType { - /// Represents class method - Class(PyMethodDef), - /// Represents static method - Static(PyMethodDef), - /// Represents normal method - Method(PyMethodDef), - /// Represents class attribute, used by `#[attribute]` - ClassAttribute(PyClassAttributeDef), - /// Represents getter descriptor, used by `#[getter]` - Getter(PyGetterDef), - /// Represents setter descriptor, used by `#[setter]` - Setter(PySetterDef), -} - -#[derive(Copy, Clone, Debug)] -pub enum PyMethodType { - PyCFunction(PyCFunction), - PyCFunctionWithKeywords(PyCFunctionWithKeywords), - #[cfg(not(Py_LIMITED_API))] - PyCFunctionFastWithKeywords(PyCFunctionFastWithKeywords), -} - -// These newtype structs serve no purpose other than wrapping which are function pointers - because -// function pointers aren't allowed in const fn, but types wrapping them are! -#[derive(Clone, Copy, Debug)] -pub struct PyCFunction(pub ffi::PyCFunction); -#[derive(Clone, Copy, Debug)] -pub struct PyCFunctionWithKeywords(pub ffi::PyCFunctionWithKeywords); -#[cfg(not(Py_LIMITED_API))] -#[derive(Clone, Copy, Debug)] -pub struct PyCFunctionFastWithKeywords(pub ffi::_PyCFunctionFastWithKeywords); -#[derive(Clone, Copy, Debug)] -pub struct PyGetter(pub ffi::getter); -#[derive(Clone, Copy, Debug)] -pub struct PySetter(pub ffi::setter); -#[derive(Clone, Copy)] -pub struct PyClassAttributeFactory(pub for<'p> fn(Python<'p>) -> PyObject); - -// TODO: it would be nice to use CStr in these types, but then the constructors can't be const fn -// until `CStr::from_bytes_with_nul_unchecked` is const fn. - -#[derive(Clone, Debug)] -pub struct PyMethodDef { - pub(crate) ml_name: &'static str, - pub(crate) ml_meth: PyMethodType, - pub(crate) ml_flags: c_int, - pub(crate) ml_doc: &'static str, -} - -#[derive(Copy, Clone)] -pub struct PyClassAttributeDef { - pub(crate) name: &'static str, - pub(crate) meth: PyClassAttributeFactory, -} - -#[derive(Clone, Debug)] -pub struct PyGetterDef { - pub(crate) name: &'static str, - pub(crate) meth: PyGetter, - doc: &'static str, -} - -#[derive(Clone, Debug)] -pub struct PySetterDef { - pub(crate) name: &'static str, - pub(crate) meth: PySetter, - doc: &'static str, -} - -unsafe impl Sync for PyMethodDef {} - -unsafe impl Sync for PyGetterDef {} - -unsafe impl Sync for PySetterDef {} - -impl PyMethodDef { - /// Define a function with no `*args` and `**kwargs`. - pub const fn noargs(name: &'static str, cfunction: PyCFunction, doc: &'static str) -> Self { - Self { - ml_name: name, - ml_meth: PyMethodType::PyCFunction(cfunction), - ml_flags: ffi::METH_NOARGS, - ml_doc: doc, - } - } - - /// Define a function that can take `*args` and `**kwargs`. - pub const fn cfunction_with_keywords( - name: &'static str, - cfunction: PyCFunctionWithKeywords, - doc: &'static str, - ) -> Self { - Self { - ml_name: name, - ml_meth: PyMethodType::PyCFunctionWithKeywords(cfunction), - ml_flags: ffi::METH_VARARGS | ffi::METH_KEYWORDS, - ml_doc: doc, - } - } - - /// Define a function that can take `*args` and `**kwargs`. - #[cfg(not(Py_LIMITED_API))] - pub const fn fastcall_cfunction_with_keywords( - name: &'static str, - cfunction: PyCFunctionFastWithKeywords, - doc: &'static str, - ) -> Self { - Self { - ml_name: name, - ml_meth: PyMethodType::PyCFunctionFastWithKeywords(cfunction), - ml_flags: ffi::METH_FASTCALL | ffi::METH_KEYWORDS, - ml_doc: doc, - } - } - - pub const fn flags(mut self, flags: c_int) -> Self { - self.ml_flags |= flags; - self - } - - /// Convert `PyMethodDef` to Python method definition struct `ffi::PyMethodDef` - pub(crate) fn as_method_def(&self) -> Result { - let meth = match self.ml_meth { - PyMethodType::PyCFunction(meth) => meth.0, - PyMethodType::PyCFunctionWithKeywords(meth) => unsafe { std::mem::transmute(meth.0) }, - #[cfg(not(Py_LIMITED_API))] - PyMethodType::PyCFunctionFastWithKeywords(meth) => unsafe { - std::mem::transmute(meth.0) - }, - }; - - Ok(ffi::PyMethodDef { - ml_name: get_name(self.ml_name)?.as_ptr(), - ml_meth: Some(meth), - ml_flags: self.ml_flags, - ml_doc: get_doc(self.ml_doc)?.as_ptr(), - }) - } -} - -impl PyClassAttributeDef { - /// Define a class attribute. - pub const fn new(name: &'static str, meth: PyClassAttributeFactory) -> Self { - Self { name, meth } - } -} - -// Manual implementation because `Python<'_>` does not implement `Debug` and -// trait bounds on `fn` compiler-generated derive impls are too restrictive. -impl fmt::Debug for PyClassAttributeDef { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("PyClassAttributeDef") - .field("name", &self.name) - .finish() - } -} - -impl PyGetterDef { - /// Define a getter. - pub const fn new(name: &'static str, getter: PyGetter, doc: &'static str) -> Self { - Self { - name, - meth: getter, - doc, - } - } - - /// Copy descriptor information to `ffi::PyGetSetDef` - pub fn copy_to(&self, dst: &mut ffi::PyGetSetDef) { - if dst.name.is_null() { - dst.name = get_name(self.name).unwrap().as_ptr() as _; - } - if dst.doc.is_null() { - dst.doc = get_doc(self.doc).unwrap().as_ptr() as _; - } - dst.get = Some(self.meth.0); - } -} - -impl PySetterDef { - /// Define a setter. - pub const fn new(name: &'static str, setter: PySetter, doc: &'static str) -> Self { - Self { - name, - meth: setter, - doc, - } - } - - /// Copy descriptor information to `ffi::PyGetSetDef` - pub fn copy_to(&self, dst: &mut ffi::PyGetSetDef) { - if dst.name.is_null() { - dst.name = get_name(self.name).unwrap().as_ptr() as _; - } - if dst.doc.is_null() { - dst.doc = get_doc(self.doc).unwrap().as_ptr() as _; - } - dst.set = Some(self.meth.0); - } -} - -fn get_name(name: &'static str) -> Result<&'static CStr, NulByteInString> { - extract_cstr_or_leak_cstring(name, "Function name cannot contain NUL byte.") -} - -fn get_doc(doc: &'static str) -> Result<&'static CStr, NulByteInString> { - extract_cstr_or_leak_cstring(doc, "Document cannot contain NUL byte.") -} diff --git a/src/class/mod.rs b/src/class/mod.rs index 0e889573..3220ad3a 100644 --- a/src/class/mod.rs +++ b/src/class/mod.rs @@ -10,12 +10,10 @@ pub mod basic; pub mod buffer; pub mod descr; pub mod gc; -#[doc(hidden)] -pub mod impl_; pub mod iter; pub mod mapping; #[doc(hidden)] -pub mod methods; +pub use crate::impl_::pymethods as methods; pub mod number; pub mod pyasync; pub mod sequence; diff --git a/src/class/pyasync.rs b/src/class/pyasync.rs index f79cd7ac..96053824 100644 --- a/src/class/pyasync.rs +++ b/src/class/pyasync.rs @@ -9,8 +9,7 @@ use crate::callback::IntoPyCallbackOutput; use crate::derive_utils::TryFromPyCell; -use crate::err::PyResult; -use crate::{ffi, IntoPy, IntoPyPointer, PyClass, PyObject, Python}; +use crate::{PyClass, PyObject}; /// Python Async/Await support interface. /// @@ -58,51 +57,4 @@ py_unarys_func!(await_, PyAsyncAwaitProtocol, Self::__await__); py_unarys_func!(aiter, PyAsyncAiterProtocol, Self::__aiter__); py_unarys_func!(anext, PyAsyncAnextProtocol, Self::__anext__); -/// Output of `__anext__`. -/// -/// -pub enum IterANextOutput { - /// An expression which the generator yielded. - Yield(T), - /// A `StopAsyncIteration` object. - Return(U), -} - -/// An [IterANextOutput] of Python objects. -pub type PyIterANextOutput = IterANextOutput; - -impl IntoPyCallbackOutput<*mut ffi::PyObject> for PyIterANextOutput { - fn convert(self, _py: Python) -> PyResult<*mut ffi::PyObject> { - match self { - IterANextOutput::Yield(o) => Ok(o.into_ptr()), - IterANextOutput::Return(opt) => { - Err(crate::exceptions::PyStopAsyncIteration::new_err((opt,))) - } - } - } -} - -impl IntoPyCallbackOutput for IterANextOutput -where - T: IntoPy, - U: IntoPy, -{ - fn convert(self, py: Python) -> PyResult { - match self { - IterANextOutput::Yield(o) => Ok(IterANextOutput::Yield(o.into_py(py))), - IterANextOutput::Return(o) => Ok(IterANextOutput::Return(o.into_py(py))), - } - } -} - -impl IntoPyCallbackOutput for Option -where - T: IntoPy, -{ - fn convert(self, py: Python) -> PyResult { - match self { - Some(o) => Ok(PyIterANextOutput::Yield(o.into_py(py))), - None => Ok(PyIterANextOutput::Return(py.None())), - } - } -} +pub use crate::pyclass::{IterANextOutput, PyIterANextOutput}; diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index ae534611..66ac7c83 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1,4 +1,13 @@ -use crate::{ffi, PyCell, PyClass, Python}; +use crate::{ + exceptions::{PyAttributeError, PyNotImplementedError}, + ffi, + impl_::freelist::FreeList, + pycell::PyCellLayout, + pyclass_init::PyObjectInit, + type_object::{PyLayout, PyTypeObject}, + PyCell, PyClass, PyMethodDefType, PyNativeType, PyResult, PyTypeInfo, Python, +}; +use std::{marker::PhantomData, os::raw::{c_int, c_void}, ptr::NonNull, thread}; /// Gets the offset of the dictionary from the start of the object in bytes. #[inline] @@ -94,3 +103,804 @@ impl PyClassWeakRef for PyClassWeakRefSlot { } } } + +/// This type is used as a "dummy" type on which dtolnay specializations are +/// applied to apply implementations from `#[pymethods]` & `#[pyproto]` +pub struct PyClassImplCollector(PhantomData); + +impl PyClassImplCollector { + pub fn new() -> Self { + Self(PhantomData) + } +} + +impl Default for PyClassImplCollector { + fn default() -> Self { + Self::new() + } +} + +impl Clone for PyClassImplCollector { + fn clone(&self) -> Self { + Self::new() + } +} + +impl Copy for PyClassImplCollector {} + +/// Implements the underlying functionality of `#[pyclass]`, assembled by various proc macros. +/// +/// Users are discouraged from implementing this trait manually; it is a PyO3 implementation detail +/// and may be changed at any time. +pub trait PyClassImpl: Sized { + /// Class doc string + const DOC: &'static str = "\0"; + + /// #[pyclass(gc)] + const IS_GC: bool = false; + + /// #[pyclass(subclass)] + const IS_BASETYPE: bool = false; + + /// #[pyclass(extends=...)] + const IS_SUBCLASS: bool = false; + + /// Layout + type Layout: PyLayout; + + /// Base class + type BaseType: PyTypeInfo + PyTypeObject + PyClassBaseType; + + /// This handles following two situations: + /// 1. In case `T` is `Send`, stub `ThreadChecker` is used and does nothing. + /// This implementation is used by default. Compile fails if `T: !Send`. + /// 2. In case `T` is `!Send`, `ThreadChecker` panics when `T` is accessed by another thread. + /// This implementation is used when `#[pyclass(unsendable)]` is given. + /// Panicking makes it safe to expose `T: !Send` to the Python interpreter, where all objects + /// can be accessed by multiple threads by `threading` module. + type ThreadChecker: PyClassThreadChecker; + + #[cfg(feature = "multiple-pymethods")] + type Inventory: PyClassInventory; + + fn for_each_method_def(_visitor: &mut dyn FnMut(&[PyMethodDefType])) {} + fn for_each_proto_slot(_visitor: &mut dyn FnMut(&[ffi::PyType_Slot])) {} + + #[inline] + fn get_new() -> Option { + None + } + #[inline] + fn get_alloc() -> Option { + None + } + #[inline] + fn get_free() -> Option { + None + } + #[inline] + fn dict_offset() -> Option { + None + } + #[inline] + fn weaklist_offset() -> Option { + None + } +} + +// Traits describing known special methods. + +pub trait PyClassNewImpl { + fn new_impl(self) -> Option; +} + +impl PyClassNewImpl for &'_ PyClassImplCollector { + fn new_impl(self) -> Option { + None + } +} + +macro_rules! slot_fragment_trait { + ($trait_name:ident, $($default_method:tt)*) => { + #[allow(non_camel_case_types)] + pub trait $trait_name: Sized { + $($default_method)* + } + + impl $trait_name for &'_ PyClassImplCollector {} + } +} + +/// Macro which expands to three items +/// - Trait for a __setitem__ dunder +/// - Trait for the corresponding __delitem__ dunder +/// - A macro which will use dtolnay specialisation to generate the shared slot for the two dunders +macro_rules! define_pyclass_setattr_slot { + ( + $set_trait:ident, + $del_trait:ident, + $set:ident, + $del:ident, + $set_error:expr, + $del_error:expr, + $generate_macro:ident, + $slot:ident, + $func_ty:ident, + ) => { + slot_fragment_trait! { + $set_trait, + + /// # Safety: _slf and _attr must be valid non-null Python objects + #[inline] + unsafe fn $set( + self, + _py: Python, + _slf: *mut ffi::PyObject, + _attr: *mut ffi::PyObject, + _value: NonNull, + ) -> PyResult<()> { + $set_error + } + } + + slot_fragment_trait! { + $del_trait, + + /// # Safety: _slf and _attr must be valid non-null Python objects + #[inline] + unsafe fn $del( + self, + _py: Python, + _slf: *mut ffi::PyObject, + _attr: *mut ffi::PyObject, + ) -> PyResult<()> { + $del_error + } + } + + #[doc(hidden)] + #[macro_export] + macro_rules! $generate_macro { + ($cls:ty) => {{ + unsafe extern "C" fn __wrap( + _slf: *mut $crate::ffi::PyObject, + attr: *mut $crate::ffi::PyObject, + value: *mut $crate::ffi::PyObject, + ) -> ::std::os::raw::c_int { + use ::std::option::Option::*; + use $crate::callback::IntoPyCallbackOutput; + use $crate::impl_::pyclass::*; + $crate::callback::handle_panic(|py| { + let collector = PyClassImplCollector::<$cls>::new(); + if let Some(value) = ::std::ptr::NonNull::new(value) { + collector.$set(py, _slf, attr, value).convert(py) + } else { + collector.$del(py, _slf, attr).convert(py) + } + }) + } + $crate::ffi::PyType_Slot { + slot: $crate::ffi::$slot, + pfunc: __wrap as $crate::ffi::$func_ty as _, + } + }}; + } + pub use $generate_macro; + }; +} + +define_pyclass_setattr_slot! { + PyClass__setattr__SlotFragment, + PyClass__delattr__SlotFragment, + __setattr__, + __delattr__, + Err(PyAttributeError::new_err("can't set attribute")), + Err(PyAttributeError::new_err("can't delete attribute")), + generate_pyclass_setattr_slot, + Py_tp_setattro, + setattrofunc, +} + +define_pyclass_setattr_slot! { + PyClass__set__SlotFragment, + PyClass__delete__SlotFragment, + __set__, + __delete__, + Err(PyNotImplementedError::new_err("can't set descriptor")), + Err(PyNotImplementedError::new_err("can't delete descriptor")), + generate_pyclass_setdescr_slot, + Py_tp_descr_set, + descrsetfunc, +} + +define_pyclass_setattr_slot! { + PyClass__setitem__SlotFragment, + PyClass__delitem__SlotFragment, + __setitem__, + __delitem__, + Err(PyNotImplementedError::new_err("can't set item")), + Err(PyNotImplementedError::new_err("can't delete item")), + generate_pyclass_setitem_slot, + Py_mp_ass_subscript, + objobjargproc, +} + +/// Macro which expands to three items +/// - Trait for a lhs dunder e.g. __add__ +/// - Trait for the corresponding rhs e.g. __radd__ +/// - A macro which will use dtolnay specialisation to generate the shared slot for the two dunders +macro_rules! define_pyclass_binary_operator_slot { + ( + $lhs_trait:ident, + $rhs_trait:ident, + $lhs:ident, + $rhs:ident, + $generate_macro:ident, + $slot:ident, + $func_ty:ident, + ) => { + slot_fragment_trait! { + $lhs_trait, + + /// # Safety: _slf and _other must be valid non-null Python objects + #[inline] + unsafe fn $lhs( + self, + _py: Python, + _slf: *mut ffi::PyObject, + _other: *mut ffi::PyObject, + ) -> PyResult<*mut ffi::PyObject> { + Ok(ffi::_Py_NewRef(ffi::Py_NotImplemented())) + } + } + + slot_fragment_trait! { + $rhs_trait, + + /// # Safety: _slf and _other must be valid non-null Python objects + #[inline] + unsafe fn $rhs( + self, + _py: Python, + _slf: *mut ffi::PyObject, + _other: *mut ffi::PyObject, + ) -> PyResult<*mut ffi::PyObject> { + Ok(ffi::_Py_NewRef(ffi::Py_NotImplemented())) + } + } + + #[doc(hidden)] + #[macro_export] + macro_rules! $generate_macro { + ($cls:ty) => {{ + unsafe extern "C" fn __wrap( + _slf: *mut $crate::ffi::PyObject, + _other: *mut $crate::ffi::PyObject, + ) -> *mut $crate::ffi::PyObject { + $crate::callback::handle_panic(|py| { + use $crate::impl_::pyclass::*; + let collector = PyClassImplCollector::<$cls>::new(); + let lhs_result = collector.$lhs(py, _slf, _other)?; + if lhs_result == $crate::ffi::Py_NotImplemented() { + $crate::ffi::Py_DECREF(lhs_result); + collector.$rhs(py, _other, _slf) + } else { + ::std::result::Result::Ok(lhs_result) + } + }) + } + $crate::ffi::PyType_Slot { + slot: $crate::ffi::$slot, + pfunc: __wrap as $crate::ffi::$func_ty as _, + } + }}; + } + pub use $generate_macro; + }; +} + +define_pyclass_binary_operator_slot! { + PyClass__add__SlotFragment, + PyClass__radd__SlotFragment, + __add__, + __radd__, + generate_pyclass_add_slot, + Py_nb_add, + binaryfunc, +} + +define_pyclass_binary_operator_slot! { + PyClass__sub__SlotFragment, + PyClass__rsub__SlotFragment, + __sub__, + __rsub__, + generate_pyclass_sub_slot, + Py_nb_subtract, + binaryfunc, +} + +define_pyclass_binary_operator_slot! { + PyClass__mul__SlotFragment, + PyClass__rmul__SlotFragment, + __mul__, + __rmul__, + generate_pyclass_mul_slot, + Py_nb_multiply, + binaryfunc, +} + +define_pyclass_binary_operator_slot! { + PyClass__mod__SlotFragment, + PyClass__rmod__SlotFragment, + __mod__, + __rmod__, + generate_pyclass_mod_slot, + Py_nb_remainder, + binaryfunc, +} + +define_pyclass_binary_operator_slot! { + PyClass__divmod__SlotFragment, + PyClass__rdivmod__SlotFragment, + __divmod__, + __rdivmod__, + generate_pyclass_divmod_slot, + Py_nb_divmod, + binaryfunc, +} + +define_pyclass_binary_operator_slot! { + PyClass__lshift__SlotFragment, + PyClass__rlshift__SlotFragment, + __lshift__, + __rlshift__, + generate_pyclass_lshift_slot, + Py_nb_lshift, + binaryfunc, +} + +define_pyclass_binary_operator_slot! { + PyClass__rshift__SlotFragment, + PyClass__rrshift__SlotFragment, + __rshift__, + __rrshift__, + generate_pyclass_rshift_slot, + Py_nb_rshift, + binaryfunc, +} + +define_pyclass_binary_operator_slot! { + PyClass__and__SlotFragment, + PyClass__rand__SlotFragment, + __and__, + __rand__, + generate_pyclass_and_slot, + Py_nb_and, + binaryfunc, +} + +define_pyclass_binary_operator_slot! { + PyClass__or__SlotFragment, + PyClass__ror__SlotFragment, + __or__, + __ror__, + generate_pyclass_or_slot, + Py_nb_or, + binaryfunc, +} + +define_pyclass_binary_operator_slot! { + PyClass__xor__SlotFragment, + PyClass__rxor__SlotFragment, + __xor__, + __rxor__, + generate_pyclass_xor_slot, + Py_nb_xor, + binaryfunc, +} + +define_pyclass_binary_operator_slot! { + PyClass__matmul__SlotFragment, + PyClass__rmatmul__SlotFragment, + __matmul__, + __rmatmul__, + generate_pyclass_matmul_slot, + Py_nb_matrix_multiply, + binaryfunc, +} + +define_pyclass_binary_operator_slot! { + PyClass__truediv__SlotFragment, + PyClass__rtruediv__SlotFragment, + __truediv__, + __rtruediv__, + generate_pyclass_truediv_slot, + Py_nb_true_divide, + binaryfunc, +} + +define_pyclass_binary_operator_slot! { + PyClass__floordiv__SlotFragment, + PyClass__rfloordiv__SlotFragment, + __floordiv__, + __rfloordiv__, + generate_pyclass_floordiv_slot, + Py_nb_floor_divide, + binaryfunc, +} + +slot_fragment_trait! { + PyClass__pow__SlotFragment, + + /// # Safety: _slf and _other must be valid non-null Python objects + #[inline] + unsafe fn __pow__( + self, + _py: Python, + _slf: *mut ffi::PyObject, + _other: *mut ffi::PyObject, + _mod: *mut ffi::PyObject, + ) -> PyResult<*mut ffi::PyObject> { + Ok(ffi::_Py_NewRef(ffi::Py_NotImplemented())) + } +} + +slot_fragment_trait! { + PyClass__rpow__SlotFragment, + + /// # Safety: _slf and _other must be valid non-null Python objects + #[inline] + unsafe fn __rpow__( + self, + _py: Python, + _slf: *mut ffi::PyObject, + _other: *mut ffi::PyObject, + _mod: *mut ffi::PyObject, + ) -> PyResult<*mut ffi::PyObject> { + Ok(ffi::_Py_NewRef(ffi::Py_NotImplemented())) + } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! generate_pyclass_pow_slot { + ($cls:ty) => {{ + unsafe extern "C" fn __wrap( + _slf: *mut $crate::ffi::PyObject, + _other: *mut $crate::ffi::PyObject, + _mod: *mut $crate::ffi::PyObject, + ) -> *mut $crate::ffi::PyObject { + $crate::callback::handle_panic(|py| { + use $crate::impl_::pyclass::*; + let collector = PyClassImplCollector::<$cls>::new(); + let lhs_result = collector.__pow__(py, _slf, _other, _mod)?; + if lhs_result == $crate::ffi::Py_NotImplemented() { + $crate::ffi::Py_DECREF(lhs_result); + collector.__rpow__(py, _other, _slf, _mod) + } else { + ::std::result::Result::Ok(lhs_result) + } + }) + } + $crate::ffi::PyType_Slot { + slot: $crate::ffi::Py_nb_power, + pfunc: __wrap as $crate::ffi::ternaryfunc as _, + } + }}; +} +pub use generate_pyclass_pow_slot; + +pub trait PyClassAllocImpl { + fn alloc_impl(self) -> Option; +} + +impl PyClassAllocImpl for &'_ PyClassImplCollector { + fn alloc_impl(self) -> Option { + None + } +} + +pub trait PyClassFreeImpl { + fn free_impl(self) -> Option; +} + +impl PyClassFreeImpl for &'_ PyClassImplCollector { + fn free_impl(self) -> Option { + None + } +} + +/// Implements a freelist. +/// +/// Do not implement this trait manually. Instead, use `#[pyclass(freelist = N)]` +/// on a Rust struct to implement it. +pub trait PyClassWithFreeList: PyClass { + fn get_free_list(py: Python) -> &mut FreeList<*mut ffi::PyObject>; +} + +/// Implementation of tp_alloc for `freelist` classes. +/// +/// # Safety +/// - `subtype` must be a valid pointer to the type object of T or a subclass. +/// - The GIL must be held. +pub unsafe extern "C" fn alloc_with_freelist( + subtype: *mut ffi::PyTypeObject, + nitems: ffi::Py_ssize_t, +) -> *mut ffi::PyObject { + let py = Python::assume_gil_acquired(); + + #[cfg(not(Py_3_8))] + bpo_35810_workaround(py, subtype); + + let self_type = T::type_object_raw(py); + // If this type is a variable type or the subtype is not equal to this type, we cannot use the + // freelist + if nitems == 0 && subtype == self_type { + if let Some(obj) = T::get_free_list(py).pop() { + ffi::PyObject_Init(obj, subtype); + return obj as _; + } + } + + ffi::PyType_GenericAlloc(subtype, nitems) +} + +/// Implementation of tp_free for `freelist` classes. +/// +/// # Safety +/// - `obj` must be a valid pointer to an instance of T (not a subclass). +/// - The GIL must be held. +pub unsafe extern "C" fn free_with_freelist(obj: *mut c_void) { + let obj = obj as *mut ffi::PyObject; + debug_assert_eq!( + T::type_object_raw(Python::assume_gil_acquired()), + ffi::Py_TYPE(obj) + ); + if let Some(obj) = T::get_free_list(Python::assume_gil_acquired()).insert(obj) { + let ty = ffi::Py_TYPE(obj); + + // Deduce appropriate inverse of PyType_GenericAlloc + let free = if ffi::PyType_IS_GC(ty) != 0 { + ffi::PyObject_GC_Del + } else { + ffi::PyObject_Free + }; + free(obj as *mut c_void); + + #[cfg(Py_3_8)] + if ffi::PyType_HasFeature(ty, ffi::Py_TPFLAGS_HEAPTYPE) != 0 { + ffi::Py_DECREF(ty as *mut ffi::PyObject); + } + } +} + +/// Workaround for Python issue 35810; no longer necessary in Python 3.8 +#[inline] +#[cfg(not(Py_3_8))] +unsafe fn bpo_35810_workaround(_py: Python, ty: *mut ffi::PyTypeObject) { + #[cfg(Py_LIMITED_API)] + { + // Must check version at runtime for abi3 wheels - they could run against a higher version + // than the build config suggests. + use crate::once_cell::GILOnceCell; + static IS_PYTHON_3_8: GILOnceCell = GILOnceCell::new(); + + if *IS_PYTHON_3_8.get_or_init(_py, || _py.version_info() >= (3, 8)) { + // No fix needed - the wheel is running on a sufficiently new interpreter. + return; + } + } + + ffi::Py_INCREF(ty as *mut ffi::PyObject); +} + +// General methods implementation: either dtolnay specialization trait or inventory if +// multiple-pymethods feature is enabled. + +macro_rules! methods_trait { + ($name:ident, $function_name: ident) => { + pub trait $name { + fn $function_name(self) -> &'static [PyMethodDefType]; + } + + impl $name for &'_ PyClassImplCollector { + fn $function_name(self) -> &'static [PyMethodDefType] { + &[] + } + } + }; +} + +/// Implementation detail. Only to be used through our proc macro code. +/// Method storage for `#[pyclass]`. +/// Allows arbitrary `#[pymethod]` blocks to submit their methods, +/// which are eventually collected by `#[pyclass]`. +#[cfg(feature = "multiple-pymethods")] +pub trait PyClassInventory: inventory::Collect { + /// Returns the methods for a single `#[pymethods] impl` block + fn methods(&'static self) -> &'static [PyMethodDefType]; + + /// Returns the slots for a single `#[pymethods] impl` block + fn slots(&'static self) -> &'static [ffi::PyType_Slot]; +} + +// Methods from #[pyo3(get, set)] on struct fields. +methods_trait!(PyClassDescriptors, py_class_descriptors); + +// Methods from #[pymethods] if not using inventory. +#[cfg(not(feature = "multiple-pymethods"))] +methods_trait!(PyMethods, py_methods); + +// All traits describing slots, as well as the fallback implementations for unimplemented protos +// +// Protos which are implemented use dtolnay specialization to implement for PyClassImplCollector. +// +// See https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md + +macro_rules! slots_trait { + ($name:ident, $function_name: ident) => { + pub trait $name { + fn $function_name(self) -> &'static [ffi::PyType_Slot]; + } + + impl $name for &'_ PyClassImplCollector { + fn $function_name(self) -> &'static [ffi::PyType_Slot] { + &[] + } + } + }; +} + +slots_trait!(PyObjectProtocolSlots, object_protocol_slots); +slots_trait!(PyDescrProtocolSlots, descr_protocol_slots); +slots_trait!(PyGCProtocolSlots, gc_protocol_slots); +slots_trait!(PyIterProtocolSlots, iter_protocol_slots); +slots_trait!(PyMappingProtocolSlots, mapping_protocol_slots); +slots_trait!(PyNumberProtocolSlots, number_protocol_slots); +slots_trait!(PyAsyncProtocolSlots, async_protocol_slots); +slots_trait!(PySequenceProtocolSlots, sequence_protocol_slots); +slots_trait!(PyBufferProtocolSlots, buffer_protocol_slots); + +// slots that PyO3 implements by default, but can be overidden by the users. +slots_trait!(PyClassDefaultSlots, py_class_default_slots); + +// Protocol slots from #[pymethods] if not using inventory. +#[cfg(not(feature = "multiple-pymethods"))] +slots_trait!(PyMethodsProtocolSlots, methods_protocol_slots); + +methods_trait!(PyObjectProtocolMethods, object_protocol_methods); +methods_trait!(PyAsyncProtocolMethods, async_protocol_methods); +methods_trait!(PyDescrProtocolMethods, descr_protocol_methods); +methods_trait!(PyMappingProtocolMethods, mapping_protocol_methods); +methods_trait!(PyNumberProtocolMethods, number_protocol_methods); + +// Thread checkers + +#[doc(hidden)] +pub trait PyClassThreadChecker: Sized { + fn ensure(&self); + fn new() -> Self; + private_decl! {} +} + +/// Stub checker for `Send` types. +#[doc(hidden)] +pub struct ThreadCheckerStub(PhantomData); + +impl PyClassThreadChecker for ThreadCheckerStub { + fn ensure(&self) {} + #[inline] + fn new() -> Self { + ThreadCheckerStub(PhantomData) + } + private_impl! {} +} + +impl PyClassThreadChecker for ThreadCheckerStub { + fn ensure(&self) {} + #[inline] + fn new() -> Self { + ThreadCheckerStub(PhantomData) + } + private_impl! {} +} + +/// Thread checker for unsendable types. +/// Panics when the value is accessed by another thread. +#[doc(hidden)] +pub struct ThreadCheckerImpl(thread::ThreadId, PhantomData); + +impl PyClassThreadChecker for ThreadCheckerImpl { + fn ensure(&self) { + if thread::current().id() != self.0 { + panic!( + "{} is unsendable, but sent to another thread!", + std::any::type_name::() + ); + } + } + fn new() -> Self { + ThreadCheckerImpl(thread::current().id(), PhantomData) + } + private_impl! {} +} + +/// Thread checker for types that have `Send` and `extends=...`. +/// Ensures that `T: Send` and the parent is not accessed by another thread. +#[doc(hidden)] +pub struct ThreadCheckerInherited(PhantomData, U::ThreadChecker); + +impl PyClassThreadChecker for ThreadCheckerInherited { + fn ensure(&self) { + self.1.ensure(); + } + fn new() -> Self { + ThreadCheckerInherited(PhantomData, U::ThreadChecker::new()) + } + private_impl! {} +} + +/// Trait denoting that this class is suitable to be used as a base type for PyClass. +pub trait PyClassBaseType: Sized { + type LayoutAsBase: PyCellLayout; + type BaseNativeType; + type ThreadChecker: PyClassThreadChecker; + type Initializer: PyObjectInit; +} + +/// All PyClasses can be used as a base type. +impl PyClassBaseType for T { + type LayoutAsBase = crate::pycell::PyCell; + type BaseNativeType = T::BaseNativeType; + type ThreadChecker = T::ThreadChecker; + type Initializer = crate::pyclass_init::PyClassInitializer; +} + +/// Default new implementation +pub(crate) unsafe extern "C" fn fallback_new( + _subtype: *mut ffi::PyTypeObject, + _args: *mut ffi::PyObject, + _kwds: *mut ffi::PyObject, +) -> *mut ffi::PyObject { + crate::callback_body!(py, { + Err::<(), _>(crate::exceptions::PyTypeError::new_err( + "No constructor defined", + )) + }) +} + +/// Implementation of tp_dealloc for all pyclasses +pub(crate) unsafe extern "C" fn tp_dealloc(obj: *mut ffi::PyObject) { + crate::callback_body!(py, T::Layout::tp_dealloc(obj, py)) +} + +pub(crate) unsafe extern "C" fn get_sequence_item_from_mapping( + obj: *mut ffi::PyObject, + index: ffi::Py_ssize_t, +) -> *mut ffi::PyObject { + let index = ffi::PyLong_FromSsize_t(index); + if index.is_null() { + return std::ptr::null_mut(); + } + let result = ffi::PyObject_GetItem(obj, index); + ffi::Py_DECREF(index); + result +} + +pub(crate) unsafe extern "C" fn assign_sequence_item_from_mapping( + obj: *mut ffi::PyObject, + index: ffi::Py_ssize_t, + value: *mut ffi::PyObject, +) -> c_int { + let index = ffi::PyLong_FromSsize_t(index); + if index.is_null() { + return -1; + } + let result = if value.is_null() { + ffi::PyObject_DelItem(obj, index) + } else { + ffi::PyObject_SetItem(obj, index, value) + }; + ffi::Py_DECREF(index); + result +} diff --git a/src/impl_/pyfunction.rs b/src/impl_/pyfunction.rs index f852e63b..01cd3f08 100644 --- a/src/impl_/pyfunction.rs +++ b/src/impl_/pyfunction.rs @@ -1,5 +1,5 @@ use crate::{ - class::methods::PyMethodDef, derive_utils::PyFunctionArguments, types::PyCFunction, PyResult, + derive_utils::PyFunctionArguments, impl_::pymethods::PyMethodDef, types::PyCFunction, PyResult, }; pub trait PyFunctionDef { diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 2a8d743a..cbaa2994 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -1,4 +1,8 @@ -use crate::{ffi, FromPyObject, PyAny, PyResult, Python}; +use crate::internal_tricks::{extract_cstr_or_leak_cstring, NulByteInString}; +use crate::{ffi, FromPyObject, PyAny, PyObject, PyResult, Python}; +use std::ffi::CStr; +use std::fmt; +use std::os::raw::c_int; /// Python 3.8 and up - __ipow__ has modulo argument correctly populated. #[cfg(Py_3_8)] @@ -31,3 +35,216 @@ impl IPowModulo { unsafe { py.from_borrowed_ptr::(ffi::Py_None()) }.extract() } } + +/// `PyMethodDefType` represents different types of Python callable objects. +/// It is used by the `#[pymethods]` and `#[pyproto]` annotations. +#[derive(Debug)] +pub enum PyMethodDefType { + /// Represents class method + Class(PyMethodDef), + /// Represents static method + Static(PyMethodDef), + /// Represents normal method + Method(PyMethodDef), + /// Represents class attribute, used by `#[attribute]` + ClassAttribute(PyClassAttributeDef), + /// Represents getter descriptor, used by `#[getter]` + Getter(PyGetterDef), + /// Represents setter descriptor, used by `#[setter]` + Setter(PySetterDef), +} + +#[derive(Copy, Clone, Debug)] +pub enum PyMethodType { + PyCFunction(PyCFunction), + PyCFunctionWithKeywords(PyCFunctionWithKeywords), + #[cfg(not(Py_LIMITED_API))] + PyCFunctionFastWithKeywords(PyCFunctionFastWithKeywords), +} + +// These newtype structs serve no purpose other than wrapping which are function pointers - because +// function pointers aren't allowed in const fn, but types wrapping them are! +#[derive(Clone, Copy, Debug)] +pub struct PyCFunction(pub ffi::PyCFunction); +#[derive(Clone, Copy, Debug)] +pub struct PyCFunctionWithKeywords(pub ffi::PyCFunctionWithKeywords); +#[cfg(not(Py_LIMITED_API))] +#[derive(Clone, Copy, Debug)] +pub struct PyCFunctionFastWithKeywords(pub ffi::_PyCFunctionFastWithKeywords); +#[derive(Clone, Copy, Debug)] +pub struct PyGetter(pub ffi::getter); +#[derive(Clone, Copy, Debug)] +pub struct PySetter(pub ffi::setter); +#[derive(Clone, Copy)] +pub struct PyClassAttributeFactory(pub for<'p> fn(Python<'p>) -> PyObject); + +// TODO: it would be nice to use CStr in these types, but then the constructors can't be const fn +// until `CStr::from_bytes_with_nul_unchecked` is const fn. + +#[derive(Clone, Debug)] +pub struct PyMethodDef { + pub(crate) ml_name: &'static str, + pub(crate) ml_meth: PyMethodType, + pub(crate) ml_flags: c_int, + pub(crate) ml_doc: &'static str, +} + +#[derive(Copy, Clone)] +pub struct PyClassAttributeDef { + pub(crate) name: &'static str, + pub(crate) meth: PyClassAttributeFactory, +} + +#[derive(Clone, Debug)] +pub struct PyGetterDef { + pub(crate) name: &'static str, + pub(crate) meth: PyGetter, + doc: &'static str, +} + +#[derive(Clone, Debug)] +pub struct PySetterDef { + pub(crate) name: &'static str, + pub(crate) meth: PySetter, + doc: &'static str, +} + +unsafe impl Sync for PyMethodDef {} + +unsafe impl Sync for PyGetterDef {} + +unsafe impl Sync for PySetterDef {} + +impl PyMethodDef { + /// Define a function with no `*args` and `**kwargs`. + pub const fn noargs(name: &'static str, cfunction: PyCFunction, doc: &'static str) -> Self { + Self { + ml_name: name, + ml_meth: PyMethodType::PyCFunction(cfunction), + ml_flags: ffi::METH_NOARGS, + ml_doc: doc, + } + } + + /// Define a function that can take `*args` and `**kwargs`. + pub const fn cfunction_with_keywords( + name: &'static str, + cfunction: PyCFunctionWithKeywords, + doc: &'static str, + ) -> Self { + Self { + ml_name: name, + ml_meth: PyMethodType::PyCFunctionWithKeywords(cfunction), + ml_flags: ffi::METH_VARARGS | ffi::METH_KEYWORDS, + ml_doc: doc, + } + } + + /// Define a function that can take `*args` and `**kwargs`. + #[cfg(not(Py_LIMITED_API))] + pub const fn fastcall_cfunction_with_keywords( + name: &'static str, + cfunction: PyCFunctionFastWithKeywords, + doc: &'static str, + ) -> Self { + Self { + ml_name: name, + ml_meth: PyMethodType::PyCFunctionFastWithKeywords(cfunction), + ml_flags: ffi::METH_FASTCALL | ffi::METH_KEYWORDS, + ml_doc: doc, + } + } + + pub const fn flags(mut self, flags: c_int) -> Self { + self.ml_flags |= flags; + self + } + + /// Convert `PyMethodDef` to Python method definition struct `ffi::PyMethodDef` + pub(crate) fn as_method_def(&self) -> Result { + let meth = match self.ml_meth { + PyMethodType::PyCFunction(meth) => meth.0, + PyMethodType::PyCFunctionWithKeywords(meth) => unsafe { std::mem::transmute(meth.0) }, + #[cfg(not(Py_LIMITED_API))] + PyMethodType::PyCFunctionFastWithKeywords(meth) => unsafe { + std::mem::transmute(meth.0) + }, + }; + + Ok(ffi::PyMethodDef { + ml_name: get_name(self.ml_name)?.as_ptr(), + ml_meth: Some(meth), + ml_flags: self.ml_flags, + ml_doc: get_doc(self.ml_doc)?.as_ptr(), + }) + } +} + +impl PyClassAttributeDef { + /// Define a class attribute. + pub const fn new(name: &'static str, meth: PyClassAttributeFactory) -> Self { + Self { name, meth } + } +} + +// Manual implementation because `Python<'_>` does not implement `Debug` and +// trait bounds on `fn` compiler-generated derive impls are too restrictive. +impl fmt::Debug for PyClassAttributeDef { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PyClassAttributeDef") + .field("name", &self.name) + .finish() + } +} + +impl PyGetterDef { + /// Define a getter. + pub const fn new(name: &'static str, getter: PyGetter, doc: &'static str) -> Self { + Self { + name, + meth: getter, + doc, + } + } + + /// Copy descriptor information to `ffi::PyGetSetDef` + pub fn copy_to(&self, dst: &mut ffi::PyGetSetDef) { + if dst.name.is_null() { + dst.name = get_name(self.name).unwrap().as_ptr() as _; + } + if dst.doc.is_null() { + dst.doc = get_doc(self.doc).unwrap().as_ptr() as _; + } + dst.get = Some(self.meth.0); + } +} + +impl PySetterDef { + /// Define a setter. + pub const fn new(name: &'static str, setter: PySetter, doc: &'static str) -> Self { + Self { + name, + meth: setter, + doc, + } + } + + /// Copy descriptor information to `ffi::PyGetSetDef` + pub fn copy_to(&self, dst: &mut ffi::PyGetSetDef) { + if dst.name.is_null() { + dst.name = get_name(self.name).unwrap().as_ptr() as _; + } + if dst.doc.is_null() { + dst.doc = get_doc(self.doc).unwrap().as_ptr() as _; + } + dst.set = Some(self.meth.0); + } +} + +fn get_name(name: &'static str) -> Result<&'static CStr, NulByteInString> { + extract_cstr_or_leak_cstring(name, "Function name cannot contain NUL byte.") +} + +fn get_doc(doc: &'static str) -> Result<&'static CStr, NulByteInString> { + extract_cstr_or_leak_cstring(doc, "Document cannot contain NUL byte.") +} diff --git a/src/lib.rs b/src/lib.rs index bb6d6ace..5f11571c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -300,6 +300,30 @@ pub use crate::python::{Python, PythonVersionInfo}; pub use crate::type_object::PyTypeInfo; pub use crate::types::PyAny; +// Old directory layout, to be rethought? +#[cfg(not(feature = "pyproto"))] +pub mod class { + #[doc(hidden)] + pub use crate::impl_::pymethods as methods; + + #[doc(hidden)] + pub use self::methods::{ + PyClassAttributeDef, PyGetterDef, PyMethodDef, PyMethodDefType, PyMethodType, PySetterDef, + }; + + pub mod basic { + pub use crate::pyclass::CompareOp; + } + + pub mod pyasync { + pub use crate::pyclass::{IterANextOutput, PyIterANextOutput}; + } + + pub mod iter { + pub use crate::pyclass::{IterNextOutput, PyIterNextOutput}; + } +} + #[cfg(feature = "macros")] #[doc(hidden)] pub use { @@ -317,6 +341,7 @@ mod internal_tricks; pub mod buffer; #[doc(hidden)] pub mod callback; +#[cfg(feature = "pyproto")] pub mod class; pub mod conversion; mod conversions; @@ -350,12 +375,16 @@ pub use crate::conversions::*; )] #[cfg(feature = "macros")] pub mod proc_macro { - pub use pyo3_macros::{pyclass, pyfunction, pymethods, pymodule, pyproto}; + #[cfg(feature = "pyproto")] + pub use pyo3_macros::pyproto; + pub use pyo3_macros::{pyclass, pyfunction, pymethods, pymodule}; } +#[cfg(all(feature = "macros", feature = "pyproto"))] +pub use pyo3_macros::pyproto; #[cfg(feature = "macros")] pub use pyo3_macros::{ - pyclass, pyfunction, pymethods, pymodule, pyproto, wrap_pyfunction, wrap_pymodule, FromPyObject, + pyclass, pyfunction, pymethods, pymodule, wrap_pyfunction, wrap_pymodule, FromPyObject, }; #[cfg(feature = "macros")] diff --git a/src/prelude.rs b/src/prelude.rs index 52f79ac6..b4dcaedf 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -22,6 +22,7 @@ pub use crate::python::Python; pub use crate::types::{PyAny, PyModule}; #[cfg(feature = "macros")] -pub use pyo3_macros::{ - pyclass, pyfunction, pymethods, pymodule, pyproto, wrap_pyfunction, FromPyObject, -}; +pub use pyo3_macros::{pyclass, pyfunction, pymethods, pymodule, wrap_pyfunction, FromPyObject}; + +#[cfg(all(feature = "macros", feature = "pyproto"))] +pub use pyo3_macros::pyproto; diff --git a/src/pycell.rs b/src/pycell.rs index a0c9e197..b8b2e395 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -175,12 +175,11 @@ //! [Interior Mutability]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html "RefCell and the Interior Mutability Pattern - The Rust Programming Language" use crate::exceptions::PyRuntimeError; -use crate::impl_::pyclass::{PyClassDict, PyClassWeakRef}; +use crate::impl_::pyclass::{PyClassBaseType, PyClassDict, PyClassThreadChecker, PyClassWeakRef}; use crate::pyclass::PyClass; use crate::pyclass_init::PyClassInitializer; use crate::type_object::{PyLayout, PySizedLayout}; use crate::types::PyAny; -use crate::{class::impl_::PyClassBaseType, class::impl_::PyClassThreadChecker}; use crate::{ conversion::{AsPyPointer, FromPyPointer, ToPyObject}, ffi::PyBaseObject_Type, diff --git a/src/pyclass.rs b/src/pyclass.rs index bbbca7f3..7381732d 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -1,12 +1,10 @@ //! `PyClass` and related traits. use crate::{ - class::impl_::{ - assign_sequence_item_from_mapping, fallback_new, get_sequence_item_from_mapping, - tp_dealloc, PyClassImpl, - }, + callback::IntoPyCallbackOutput, ffi, - impl_::pyclass::{PyClassDict, PyClassWeakRef}, - PyCell, PyErr, PyMethodDefType, PyNativeType, PyResult, PyTypeInfo, Python, + impl_::pyclass::{assign_sequence_item_from_mapping, get_sequence_item_from_mapping, fallback_new, tp_dealloc, PyClassDict, PyClassImpl, PyClassWeakRef}, + IntoPy, IntoPyPointer, PyCell, PyErr, PyMethodDefType, PyNativeType, PyObject, PyResult, + PyTypeInfo, Python, }; use std::{ convert::TryInto, @@ -435,3 +433,130 @@ const PY_GET_SET_DEF_INIT: ffi::PyGetSetDef = ffi::PyGetSetDef { doc: ptr::null_mut(), closure: ptr::null_mut(), }; + +/// Operators for the `__richcmp__` method +#[derive(Debug, Clone, Copy)] +pub enum CompareOp { + /// The *less than* operator. + Lt = ffi::Py_LT as isize, + /// The *less than or equal to* operator. + Le = ffi::Py_LE as isize, + /// The equality operator. + Eq = ffi::Py_EQ as isize, + /// The *not equal to* operator. + Ne = ffi::Py_NE as isize, + /// The *greater than* operator. + Gt = ffi::Py_GT as isize, + /// The *greater than or equal to* operator. + Ge = ffi::Py_GE as isize, +} + +impl CompareOp { + pub fn from_raw(op: c_int) -> Option { + match op { + ffi::Py_LT => Some(CompareOp::Lt), + ffi::Py_LE => Some(CompareOp::Le), + ffi::Py_EQ => Some(CompareOp::Eq), + ffi::Py_NE => Some(CompareOp::Ne), + ffi::Py_GT => Some(CompareOp::Gt), + ffi::Py_GE => Some(CompareOp::Ge), + _ => None, + } + } +} + +/// Output of `__next__` which can either `yield` the next value in the iteration, or +/// `return` a value to raise `StopIteration` in Python. +/// +/// See [`PyIterProtocol`](trait.PyIterProtocol.html) for an example. +pub enum IterNextOutput { + /// The value yielded by the iterator. + Yield(T), + /// The `StopIteration` object. + Return(U), +} + +pub type PyIterNextOutput = IterNextOutput; + +impl IntoPyCallbackOutput<*mut ffi::PyObject> for PyIterNextOutput { + fn convert(self, _py: Python) -> PyResult<*mut ffi::PyObject> { + match self { + IterNextOutput::Yield(o) => Ok(o.into_ptr()), + IterNextOutput::Return(opt) => Err(crate::exceptions::PyStopIteration::new_err((opt,))), + } + } +} + +impl IntoPyCallbackOutput for IterNextOutput +where + T: IntoPy, + U: IntoPy, +{ + fn convert(self, py: Python) -> PyResult { + match self { + IterNextOutput::Yield(o) => Ok(IterNextOutput::Yield(o.into_py(py))), + IterNextOutput::Return(o) => Ok(IterNextOutput::Return(o.into_py(py))), + } + } +} + +impl IntoPyCallbackOutput for Option +where + T: IntoPy, +{ + fn convert(self, py: Python) -> PyResult { + match self { + Some(o) => Ok(PyIterNextOutput::Yield(o.into_py(py))), + None => Ok(PyIterNextOutput::Return(py.None())), + } + } +} + +/// Output of `__anext__`. +/// +/// +pub enum IterANextOutput { + /// An expression which the generator yielded. + Yield(T), + /// A `StopAsyncIteration` object. + Return(U), +} + +/// An [IterANextOutput] of Python objects. +pub type PyIterANextOutput = IterANextOutput; + +impl IntoPyCallbackOutput<*mut ffi::PyObject> for PyIterANextOutput { + fn convert(self, _py: Python) -> PyResult<*mut ffi::PyObject> { + match self { + IterANextOutput::Yield(o) => Ok(o.into_ptr()), + IterANextOutput::Return(opt) => { + Err(crate::exceptions::PyStopAsyncIteration::new_err((opt,))) + } + } + } +} + +impl IntoPyCallbackOutput for IterANextOutput +where + T: IntoPy, + U: IntoPy, +{ + fn convert(self, py: Python) -> PyResult { + match self { + IterANextOutput::Yield(o) => Ok(IterANextOutput::Yield(o.into_py(py))), + IterANextOutput::Return(o) => Ok(IterANextOutput::Return(o.into_py(py))), + } + } +} + +impl IntoPyCallbackOutput for Option +where + T: IntoPy, +{ + fn convert(self, py: Python) -> PyResult { + match self { + Some(o) => Ok(PyIterANextOutput::Yield(o.into_py(py))), + None => Ok(PyIterANextOutput::Return(py.None())), + } + } +} diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index 5fb84a96..b8b2025b 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -1,7 +1,6 @@ //! Contains initialization utilities for `#[pyclass]`. -use crate::class::impl_::PyClassThreadChecker; -use crate::impl_::pyclass::{PyClassDict, PyClassWeakRef}; -use crate::{callback::IntoPyCallbackOutput, class::impl_::PyClassBaseType}; +use crate::callback::IntoPyCallbackOutput; +use crate::impl_::pyclass::{PyClassBaseType, PyClassDict, PyClassThreadChecker, PyClassWeakRef}; use crate::{ffi, PyCell, PyClass, PyErr, PyResult, Python}; use crate::{ ffi::PyTypeObject, diff --git a/src/types/function.rs b/src/types/function.rs index fb31b087..4e72eefe 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -2,8 +2,9 @@ use crate::derive_utils::PyFunctionArguments; use crate::exceptions::PyValueError; use crate::prelude::*; use crate::{ - class::methods::{self, PyMethodDef}, - ffi, types, AsPyPointer, + ffi, + impl_::pymethods::{self, PyMethodDef}, + types, AsPyPointer, }; use std::os::raw::c_void; @@ -64,7 +65,11 @@ impl PyCFunction { py_or_module: PyFunctionArguments<'a>, ) -> PyResult<&'a Self> { Self::internal_new( - PyMethodDef::cfunction_with_keywords(name, methods::PyCFunctionWithKeywords(fun), doc), + PyMethodDef::cfunction_with_keywords( + name, + pymethods::PyCFunctionWithKeywords(fun), + doc, + ), py_or_module, ) } @@ -77,7 +82,7 @@ impl PyCFunction { py_or_module: PyFunctionArguments<'a>, ) -> PyResult<&'a Self> { Self::internal_new( - PyMethodDef::noargs(name, methods::PyCFunction(fun), doc), + PyMethodDef::noargs(name, pymethods::PyCFunction(fun), doc), py_or_module, ) } @@ -115,9 +120,9 @@ impl PyCFunction { ), )? }; - let method_def = methods::PyMethodDef::cfunction_with_keywords( + let method_def = pymethods::PyMethodDef::cfunction_with_keywords( "pyo3-closure", - methods::PyCFunctionWithKeywords(run_closure::), + pymethods::PyCFunctionWithKeywords(run_closure::), "", ); Self::internal_new_from_pointers(method_def, py, capsule.as_ptr(), std::ptr::null_mut()) diff --git a/src/types/mod.rs b/src/types/mod.rs index d2765bac..1058a16a 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -196,10 +196,10 @@ macro_rules! pyobject_native_type_sized { ($name:ty, $layout:path $(;$generics:ident)*) => { unsafe impl $crate::type_object::PyLayout<$name> for $layout {} impl $crate::type_object::PySizedLayout<$name> for $layout {} - impl<'a, $($generics,)*> $crate::class::impl_::PyClassBaseType for $name { + impl<'a, $($generics,)*> $crate::impl_::pyclass::PyClassBaseType for $name { type LayoutAsBase = $crate::pycell::PyCellBase<$layout>; type BaseNativeType = $name; - type ThreadChecker = $crate::class::impl_::ThreadCheckerStub<$crate::PyObject>; + type ThreadChecker = $crate::impl_::pyclass::ThreadCheckerStub<$crate::PyObject>; type Initializer = $crate::pyclass_init::PyNativeTypeInitializer; } } diff --git a/tests/test_arithmetics_protos.rs b/tests/test_arithmetics_protos.rs index 86b30c13..b991599a 100644 --- a/tests/test_arithmetics_protos.rs +++ b/tests/test_arithmetics_protos.rs @@ -1,5 +1,6 @@ #![allow(deprecated)] // for deprecated protocol methods #![cfg(feature = "macros")] +#![cfg(feature = "pyproto")] use pyo3::class::basic::CompareOp; use pyo3::class::*; diff --git a/tests/test_buffer.rs b/tests/test_buffer.rs index bfdf39fb..70e468c9 100644 --- a/tests/test_buffer.rs +++ b/tests/test_buffer.rs @@ -1,10 +1,7 @@ #![cfg(feature = "macros")] #![cfg(not(Py_LIMITED_API))] -use pyo3::{ - buffer::PyBuffer, class::PyBufferProtocol, exceptions::PyBufferError, ffi, prelude::*, - AsPyPointer, -}; +use pyo3::{buffer::PyBuffer, exceptions::PyBufferError, ffi, prelude::*, AsPyPointer}; use std::{ ffi::CStr, os::raw::{c_int, c_void}, @@ -28,9 +25,13 @@ struct TestBufferErrors { error: Option, } -#[pyproto] -impl PyBufferProtocol for TestBufferErrors { - fn bf_getbuffer(slf: PyRefMut, view: *mut ffi::Py_buffer, flags: c_int) -> PyResult<()> { +#[pymethods] +impl TestBufferErrors { + unsafe fn __getbuffer__( + slf: PyRefMut, + view: *mut ffi::Py_buffer, + flags: c_int, + ) -> PyResult<()> { if view.is_null() { return Err(PyBufferError::new_err("View is null")); } @@ -39,53 +40,47 @@ impl PyBufferProtocol for TestBufferErrors { return Err(PyBufferError::new_err("Object is not writable")); } - unsafe { - (*view).obj = ffi::_Py_NewRef(slf.as_ptr()); - } + (*view).obj = ffi::_Py_NewRef(slf.as_ptr()); let bytes = &slf.buf; - unsafe { - (*view).buf = bytes.as_ptr() as *mut c_void; - (*view).len = bytes.len() as isize; - (*view).readonly = 1; - (*view).itemsize = std::mem::size_of::() as isize; + (*view).buf = bytes.as_ptr() as *mut c_void; + (*view).len = bytes.len() as isize; + (*view).readonly = 1; + (*view).itemsize = std::mem::size_of::() as isize; - let msg = CStr::from_bytes_with_nul(b"I\0").unwrap(); - (*view).format = msg.as_ptr() as *mut _; + let msg = CStr::from_bytes_with_nul(b"I\0").unwrap(); + (*view).format = msg.as_ptr() as *mut _; - (*view).ndim = 1; - (*view).shape = &mut (*view).len; + (*view).ndim = 1; + (*view).shape = &mut (*view).len; - (*view).strides = &mut (*view).itemsize; + (*view).strides = &mut (*view).itemsize; - (*view).suboffsets = ptr::null_mut(); - (*view).internal = ptr::null_mut(); + (*view).suboffsets = ptr::null_mut(); + (*view).internal = ptr::null_mut(); - if let Some(err) = &slf.error { - use TestGetBufferError::*; - match err { - NullShape => { - (*view).shape = std::ptr::null_mut(); - } - NullStrides => { - (*view).strides = std::ptr::null_mut(); - } - IncorrectItemSize => { - (*view).itemsize += 1; - } - IncorrectFormat => { - (*view).format = CStr::from_bytes_with_nul(b"B\0").unwrap().as_ptr() as _; - } - IncorrectAlignment => (*view).buf = (*view).buf.add(1), + if let Some(err) = &slf.error { + use TestGetBufferError::*; + match err { + NullShape => { + (*view).shape = std::ptr::null_mut(); } + NullStrides => { + (*view).strides = std::ptr::null_mut(); + } + IncorrectItemSize => { + (*view).itemsize += 1; + } + IncorrectFormat => { + (*view).format = CStr::from_bytes_with_nul(b"B\0").unwrap().as_ptr() as _; + } + IncorrectAlignment => (*view).buf = (*view).buf.add(1), } } Ok(()) } - - fn bf_releasebuffer(_slf: PyRefMut, _view: *mut ffi::Py_buffer) {} } #[test] diff --git a/tests/test_buffer_protocol_pyproto.rs b/tests/test_buffer_protocol_pyproto.rs index 81c5a7f0..1ee36f15 100644 --- a/tests/test_buffer_protocol_pyproto.rs +++ b/tests/test_buffer_protocol_pyproto.rs @@ -1,4 +1,5 @@ #![cfg(feature = "macros")] +#![cfg(feature = "pyproto")] #![cfg(not(Py_LIMITED_API))] use pyo3::buffer::PyBuffer; diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index 0a213acb..17428ced 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -3,7 +3,6 @@ use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::types::{PyDict, PyString, PyTuple}; -use pyo3::PyMappingProtocol; #[macro_use] mod common; @@ -39,8 +38,8 @@ pub struct PyA { foo: Option, } -#[pyproto] -impl PyMappingProtocol for PyA { +#[pymethods] +impl PyA { fn __getitem__(&self, key: String) -> pyo3::PyResult { if key == "t" { Ok("bar".into()) diff --git a/tests/test_gc.rs b/tests/test_gc.rs index bf0ca182..92854473 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -1,4 +1,5 @@ #![cfg(feature = "macros")] +#![cfg(feature = "pyproto")] // FIXME: #[pymethods] to support gc protocol use pyo3::class::PyGCProtocol; use pyo3::class::PyTraverseError; diff --git a/tests/test_gc_pyproto.rs b/tests/test_gc_pyproto.rs new file mode 100644 index 00000000..cf6793b7 --- /dev/null +++ b/tests/test_gc_pyproto.rs @@ -0,0 +1,282 @@ +#![cfg(feature = "macros")] +#![cfg(feature = "pyproto")] + +use pyo3::class::PyGCProtocol; +use pyo3::class::PyTraverseError; +use pyo3::class::PyVisit; +use pyo3::prelude::*; +use pyo3::type_object::PyTypeObject; +use pyo3::{py_run, AsPyPointer, PyCell, PyTryInto}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +mod common; + +#[pyclass(freelist = 2)] +struct ClassWithFreelist {} + +#[test] +fn class_with_freelist() { + let ptr; + { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let inst = Py::new(py, ClassWithFreelist {}).unwrap(); + let _inst2 = Py::new(py, ClassWithFreelist {}).unwrap(); + ptr = inst.as_ptr(); + drop(inst); + } + + { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let inst3 = Py::new(py, ClassWithFreelist {}).unwrap(); + assert_eq!(ptr, inst3.as_ptr()); + + let inst4 = Py::new(py, ClassWithFreelist {}).unwrap(); + assert_ne!(ptr, inst4.as_ptr()) + } +} + +struct TestDropCall { + drop_called: Arc, +} + +impl Drop for TestDropCall { + fn drop(&mut self) { + self.drop_called.store(true, Ordering::Relaxed); + } +} + +#[allow(dead_code)] +#[pyclass] +struct DataIsDropped { + member1: TestDropCall, + member2: TestDropCall, +} + +#[test] +fn data_is_dropped() { + let drop_called1 = Arc::new(AtomicBool::new(false)); + let drop_called2 = Arc::new(AtomicBool::new(false)); + + { + let gil = Python::acquire_gil(); + let py = gil.python(); + let data_is_dropped = DataIsDropped { + member1: TestDropCall { + drop_called: Arc::clone(&drop_called1), + }, + member2: TestDropCall { + drop_called: Arc::clone(&drop_called2), + }, + }; + let inst = Py::new(py, data_is_dropped).unwrap(); + assert!(!drop_called1.load(Ordering::Relaxed)); + assert!(!drop_called2.load(Ordering::Relaxed)); + drop(inst); + } + + assert!(drop_called1.load(Ordering::Relaxed)); + assert!(drop_called2.load(Ordering::Relaxed)); +} + +#[allow(dead_code)] +#[pyclass] +struct GcIntegration { + self_ref: PyObject, + dropped: TestDropCall, +} + +#[pyproto] +impl PyGCProtocol for GcIntegration { + fn __traverse__(&self, visit: PyVisit) -> Result<(), PyTraverseError> { + visit.call(&self.self_ref) + } + + fn __clear__(&mut self) { + let gil = Python::acquire_gil(); + self.self_ref = gil.python().None(); + } +} + +#[test] +fn gc_integration() { + let drop_called = Arc::new(AtomicBool::new(false)); + + { + let gil = Python::acquire_gil(); + let py = gil.python(); + let inst = PyCell::new( + py, + GcIntegration { + self_ref: py.None(), + dropped: TestDropCall { + drop_called: Arc::clone(&drop_called), + }, + }, + ) + .unwrap(); + + let mut borrow = inst.borrow_mut(); + borrow.self_ref = inst.to_object(py); + } + + let gil = Python::acquire_gil(); + let py = gil.python(); + py.run("import gc; gc.collect()", None, None).unwrap(); + assert!(drop_called.load(Ordering::Relaxed)); +} + +#[pyclass(gc)] +struct GcIntegration2 {} + +#[pyproto] +impl PyGCProtocol for GcIntegration2 { + fn __traverse__(&self, _visit: PyVisit) -> Result<(), PyTraverseError> { + Ok(()) + } + fn __clear__(&mut self) {} +} + +#[test] +fn gc_integration2() { + let gil = Python::acquire_gil(); + let py = gil.python(); + let inst = PyCell::new(py, GcIntegration2 {}).unwrap(); + py_run!(py, inst, "import gc; assert inst in gc.get_objects()"); +} + +#[pyclass(subclass)] +struct BaseClassWithDrop { + data: Option>, +} + +#[pymethods] +impl BaseClassWithDrop { + #[new] + fn new() -> BaseClassWithDrop { + BaseClassWithDrop { data: None } + } +} + +impl Drop for BaseClassWithDrop { + fn drop(&mut self) { + if let Some(data) = &self.data { + data.store(true, Ordering::Relaxed); + } + } +} + +#[pyclass(extends = BaseClassWithDrop)] +struct SubClassWithDrop { + data: Option>, +} + +#[pymethods] +impl SubClassWithDrop { + #[new] + fn new() -> (Self, BaseClassWithDrop) { + ( + SubClassWithDrop { data: None }, + BaseClassWithDrop { data: None }, + ) + } +} + +impl Drop for SubClassWithDrop { + fn drop(&mut self) { + if let Some(data) = &self.data { + data.store(true, Ordering::Relaxed); + } + } +} + +#[test] +fn inheritance_with_new_methods_with_drop() { + let drop_called1 = Arc::new(AtomicBool::new(false)); + let drop_called2 = Arc::new(AtomicBool::new(false)); + + { + let gil = Python::acquire_gil(); + let py = gil.python(); + let _typebase = py.get_type::(); + let typeobj = py.get_type::(); + let inst = typeobj.call((), None).unwrap(); + + let obj: &PyCell = inst.try_into().unwrap(); + let mut obj_ref_mut = obj.borrow_mut(); + obj_ref_mut.data = Some(Arc::clone(&drop_called1)); + let base: &mut BaseClassWithDrop = obj_ref_mut.as_mut(); + base.data = Some(Arc::clone(&drop_called2)); + } + + assert!(drop_called1.load(Ordering::Relaxed)); + assert!(drop_called2.load(Ordering::Relaxed)); +} + +#[pyclass(gc)] +struct TraversableClass { + traversed: AtomicBool, +} + +impl TraversableClass { + fn new() -> Self { + Self { + traversed: AtomicBool::new(false), + } + } +} + +#[pyproto] +impl PyGCProtocol for TraversableClass { + fn __clear__(&mut self) {} + fn __traverse__(&self, _visit: PyVisit) -> Result<(), PyTraverseError> { + self.traversed.store(true, Ordering::Relaxed); + Ok(()) + } +} + +unsafe fn get_type_traverse(tp: *mut pyo3::ffi::PyTypeObject) -> Option { + std::mem::transmute(pyo3::ffi::PyType_GetSlot(tp, pyo3::ffi::Py_tp_traverse)) +} + +#[test] +fn gc_during_borrow() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + unsafe { + // declare a dummy visitor function + extern "C" fn novisit( + _object: *mut pyo3::ffi::PyObject, + _arg: *mut core::ffi::c_void, + ) -> std::os::raw::c_int { + 0 + } + + // get the traverse function + let ty = TraversableClass::type_object(py).as_type_ptr(); + let traverse = get_type_traverse(ty).unwrap(); + + // create an object and check that traversing it works normally + // when it's not borrowed + let cell = PyCell::new(py, TraversableClass::new()).unwrap(); + let obj = cell.to_object(py); + assert!(!cell.borrow().traversed.load(Ordering::Relaxed)); + traverse(obj.as_ptr(), novisit, std::ptr::null_mut()); + assert!(cell.borrow().traversed.load(Ordering::Relaxed)); + + // create an object and check that it is not traversed if the GC + // is invoked while it is already borrowed mutably + let cell2 = PyCell::new(py, TraversableClass::new()).unwrap(); + let obj2 = cell2.to_object(py); + let guard = cell2.borrow_mut(); + assert!(!guard.traversed.load(Ordering::Relaxed)); + traverse(obj2.as_ptr(), novisit, std::ptr::null_mut()); + assert!(!guard.traversed.load(Ordering::Relaxed)); + drop(guard); + } +} diff --git a/tests/test_mapping.rs b/tests/test_mapping.rs index 221496b5..0e65f60d 100644 --- a/tests/test_mapping.rs +++ b/tests/test_mapping.rs @@ -7,7 +7,6 @@ use pyo3::prelude::*; use pyo3::py_run; use pyo3::types::IntoPyDict; use pyo3::types::PyList; -use pyo3::PyMappingProtocol; mod common; @@ -33,10 +32,7 @@ impl Mapping { }) } } -} -#[pyproto] -impl PyMappingProtocol for Mapping { fn __len__(&self) -> usize { self.index.len() } diff --git a/tests/test_mapping_pyproto.rs b/tests/test_mapping_pyproto.rs new file mode 100644 index 00000000..f82b876a --- /dev/null +++ b/tests/test_mapping_pyproto.rs @@ -0,0 +1,110 @@ +#![cfg(feature = "macros")] +#![cfg(feature = "pyproto")] + +use std::collections::HashMap; + +use pyo3::exceptions::PyKeyError; +use pyo3::prelude::*; +use pyo3::py_run; +use pyo3::types::IntoPyDict; +use pyo3::types::PyList; +use pyo3::PyMappingProtocol; + +mod common; + +#[pyclass] +struct Mapping { + index: HashMap, +} + +#[pymethods] +impl Mapping { + #[new] + fn new(elements: Option<&PyList>) -> PyResult { + if let Some(pylist) = elements { + let mut elems = HashMap::with_capacity(pylist.len()); + for (i, pyelem) in pylist.into_iter().enumerate() { + let elem = String::extract(pyelem)?; + elems.insert(elem, i); + } + Ok(Self { index: elems }) + } else { + Ok(Self { + index: HashMap::new(), + }) + } + } +} + +#[pyproto] +impl PyMappingProtocol for Mapping { + fn __len__(&self) -> usize { + self.index.len() + } + + fn __getitem__(&self, query: String) -> PyResult { + self.index + .get(&query) + .copied() + .ok_or_else(|| PyKeyError::new_err("unknown key")) + } + + fn __setitem__(&mut self, key: String, value: usize) { + self.index.insert(key, value); + } + + fn __delitem__(&mut self, key: String) -> PyResult<()> { + if self.index.remove(&key).is_none() { + Err(PyKeyError::new_err("unknown key")) + } else { + Ok(()) + } + } +} + +/// Return a dict with `m = Mapping(['1', '2', '3'])`. +fn map_dict(py: Python) -> &pyo3::types::PyDict { + let d = [("Mapping", py.get_type::())].into_py_dict(py); + py_run!(py, *d, "m = Mapping(['1', '2', '3'])"); + d +} + +#[test] +fn test_getitem() { + let gil = Python::acquire_gil(); + let py = gil.python(); + let d = map_dict(py); + + py_assert!(py, *d, "m['1'] == 0"); + py_assert!(py, *d, "m['2'] == 1"); + py_assert!(py, *d, "m['3'] == 2"); + py_expect_exception!(py, *d, "print(m['4'])", PyKeyError); +} + +#[test] +fn test_setitem() { + let gil = Python::acquire_gil(); + let py = gil.python(); + let d = map_dict(py); + + py_run!(py, *d, "m['1'] = 4; assert m['1'] == 4"); + py_run!(py, *d, "m['0'] = 0; assert m['0'] == 0"); + py_assert!(py, *d, "len(m) == 4"); + py_expect_exception!(py, *d, "m[0] = 'hello'", PyTypeError); + py_expect_exception!(py, *d, "m[0] = -1", PyTypeError); +} + +#[test] +fn test_delitem() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let d = map_dict(py); + py_run!( + py, + *d, + "del m['1']; assert len(m) == 2 and m['2'] == 1 and m['3'] == 2" + ); + py_expect_exception!(py, *d, "del m[-1]", PyTypeError); + py_expect_exception!(py, *d, "del m['4']", PyKeyError); +} diff --git a/tests/test_pyproto.rs b/tests/test_pyproto.rs index 7e15d73e..0d963c1a 100644 --- a/tests/test_pyproto.rs +++ b/tests/test_pyproto.rs @@ -1,4 +1,5 @@ #![cfg(feature = "macros")] +#![cfg(feature = "pyproto")] use pyo3::class::{ PyAsyncProtocol, PyDescrProtocol, PyIterProtocol, PyMappingProtocol, PyObjectProtocol, diff --git a/tests/test_pyself.rs b/tests/test_pyself.rs index 7e2eb50f..86f4f291 100644 --- a/tests/test_pyself.rs +++ b/tests/test_pyself.rs @@ -3,7 +3,7 @@ //! Test slf: PyRef/PyMutRef(especially, slf.into::) works use pyo3::prelude::*; use pyo3::types::{PyBytes, PyString}; -use pyo3::{PyCell, PyIterProtocol}; +use pyo3::PyCell; use std::collections::HashMap; mod common; @@ -54,9 +54,9 @@ struct Iter { idx: usize, } -#[pyproto] -impl PyIterProtocol for Iter { - fn __iter__(slf: PyRef) -> PyRef<'p, Self> { +#[pymethods] +impl Iter { + fn __iter__(slf: PyRef) -> PyRef { slf } diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index eebd8650..e156b9ff 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -1,4 +1,5 @@ #![cfg(feature = "macros")] +#![cfg(feature = "pyproto")] // FIXME: change this to use #[pymethods] once supports sequence protocol use pyo3::class::PySequenceProtocol; use pyo3::exceptions::{PyIndexError, PyValueError}; diff --git a/tests/test_sequence_pyproto.rs b/tests/test_sequence_pyproto.rs new file mode 100644 index 00000000..db4b6d41 --- /dev/null +++ b/tests/test_sequence_pyproto.rs @@ -0,0 +1,304 @@ +#![cfg(feature = "macros")] +#![cfg(feature = "pyproto")] + +use pyo3::class::PySequenceProtocol; +use pyo3::exceptions::{PyIndexError, PyValueError}; +use pyo3::prelude::*; +use pyo3::types::{IntoPyDict, PyList}; + +use pyo3::py_run; + +mod common; + +#[pyclass] +struct ByteSequence { + elements: Vec, +} + +#[pymethods] +impl ByteSequence { + #[new] + fn new(elements: Option<&PyList>) -> PyResult { + if let Some(pylist) = elements { + let mut elems = Vec::with_capacity(pylist.len()); + for pyelem in pylist.into_iter() { + let elem = u8::extract(pyelem)?; + elems.push(elem); + } + Ok(Self { elements: elems }) + } else { + Ok(Self { + elements: Vec::new(), + }) + } + } +} + +#[pyproto] +impl PySequenceProtocol for ByteSequence { + fn __len__(&self) -> usize { + self.elements.len() + } + + fn __getitem__(&self, idx: isize) -> PyResult { + self.elements + .get(idx as usize) + .copied() + .ok_or_else(|| PyIndexError::new_err("list index out of range")) + } + + fn __setitem__(&mut self, idx: isize, value: u8) { + self.elements[idx as usize] = value; + } + + fn __delitem__(&mut self, idx: isize) -> PyResult<()> { + if (idx < self.elements.len() as isize) && (idx >= 0) { + self.elements.remove(idx as usize); + Ok(()) + } else { + Err(PyIndexError::new_err("list index out of range")) + } + } + + fn __contains__(&self, other: &PyAny) -> bool { + match u8::extract(other) { + Ok(x) => self.elements.contains(&x), + Err(_) => false, + } + } + + fn __concat__(&self, other: PyRef<'p, Self>) -> Self { + let mut elements = self.elements.clone(); + elements.extend_from_slice(&other.elements); + Self { elements } + } + + fn __repeat__(&self, count: isize) -> PyResult { + if count >= 0 { + let mut elements = Vec::with_capacity(self.elements.len() * count as usize); + for _ in 0..count { + elements.extend(&self.elements); + } + Ok(Self { elements }) + } else { + Err(PyValueError::new_err("invalid repeat count")) + } + } +} + +/// Return a dict with `s = ByteSequence([1, 2, 3])`. +fn seq_dict(py: Python) -> &pyo3::types::PyDict { + let d = [("ByteSequence", py.get_type::())].into_py_dict(py); + // Though we can construct `s` in Rust, let's test `__new__` works. + py_run!(py, *d, "s = ByteSequence([1, 2, 3])"); + d +} + +#[test] +fn test_getitem() { + let gil = Python::acquire_gil(); + let py = gil.python(); + let d = seq_dict(py); + + py_assert!(py, *d, "s[0] == 1"); + py_assert!(py, *d, "s[1] == 2"); + py_assert!(py, *d, "s[2] == 3"); + py_expect_exception!(py, *d, "print(s[-4])", PyIndexError); + py_expect_exception!(py, *d, "print(s[4])", PyIndexError); +} + +#[test] +fn test_setitem() { + let gil = Python::acquire_gil(); + let py = gil.python(); + let d = seq_dict(py); + + py_run!(py, *d, "s[0] = 4; assert list(s) == [4, 2, 3]"); + py_expect_exception!(py, *d, "s[0] = 'hello'", PyTypeError); +} + +#[test] +fn test_delitem() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let d = [("ByteSequence", py.get_type::())].into_py_dict(py); + + py_run!( + py, + *d, + "s = ByteSequence([1, 2, 3]); del s[0]; assert list(s) == [2, 3]" + ); + py_run!( + py, + *d, + "s = ByteSequence([1, 2, 3]); del s[1]; assert list(s) == [1, 3]" + ); + py_run!( + py, + *d, + "s = ByteSequence([1, 2, 3]); del s[-1]; assert list(s) == [1, 2]" + ); + py_run!( + py, + *d, + "s = ByteSequence([1, 2, 3]); del s[-2]; assert list(s) == [1, 3]" + ); + py_expect_exception!( + py, + *d, + "s = ByteSequence([1, 2, 3]); del s[-4]; print(list(s))", + PyIndexError + ); + py_expect_exception!( + py, + *d, + "s = ByteSequence([1, 2, 3]); del s[4]", + PyIndexError + ); +} + +#[test] +fn test_contains() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let d = seq_dict(py); + + py_assert!(py, *d, "1 in s"); + py_assert!(py, *d, "2 in s"); + py_assert!(py, *d, "3 in s"); + py_assert!(py, *d, "4 not in s"); + py_assert!(py, *d, "'hello' not in s"); +} + +#[test] +fn test_concat() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let d = seq_dict(py); + + py_run!( + py, + *d, + "s1 = ByteSequence([1, 2]); s2 = ByteSequence([3, 4]); assert list(s1 + s2) == [1, 2, 3, 4]" + ); + py_expect_exception!( + py, + *d, + "s1 = ByteSequence([1, 2]); s2 = 'hello'; s1 + s2", + PyTypeError + ); +} + +#[test] +fn test_inplace_concat() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let d = seq_dict(py); + + py_run!( + py, + *d, + "s += ByteSequence([4, 5]); assert list(s) == [1, 2, 3, 4, 5]" + ); + py_expect_exception!(py, *d, "s += 'hello'", PyTypeError); +} + +#[test] +fn test_repeat() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let d = seq_dict(py); + + py_run!(py, *d, "s2 = s * 2; assert list(s2) == [1, 2, 3, 1, 2, 3]"); + py_expect_exception!(py, *d, "s2 = s * -1", PyValueError); +} + +#[test] +fn test_inplace_repeat() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let d = [("ByteSequence", py.get_type::())].into_py_dict(py); + + py_run!( + py, + *d, + "s = ByteSequence([1, 2]); s *= 3; assert list(s) == [1, 2, 1, 2, 1, 2]" + ); + py_expect_exception!(py, *d, "s = ByteSequence([1, 2]); s *= -1", PyValueError); +} + +// Check that #[pyo3(get, set)] works correctly for Vec + +#[pyclass] +struct GenericList { + #[pyo3(get, set)] + items: Vec, +} + +#[test] +fn test_generic_list_get() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let list: PyObject = GenericList { + items: [1, 2, 3].iter().map(|i| i.to_object(py)).collect(), + } + .into_py(py); + + py_assert!(py, list, "list.items == [1, 2, 3]"); +} + +#[test] +fn test_generic_list_set() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let list = PyCell::new(py, GenericList { items: vec![] }).unwrap(); + + py_run!(py, list, "list.items = [1, 2, 3]"); + assert_eq!( + list.borrow().items, + vec![1.to_object(py), 2.to_object(py), 3.to_object(py)] + ); +} + +#[pyclass] +struct OptionList { + #[pyo3(get, set)] + items: Vec>, +} + +#[pyproto] +impl PySequenceProtocol for OptionList { + fn __getitem__(&self, idx: isize) -> PyResult> { + match self.items.get(idx as usize) { + Some(x) => Ok(*x), + None => Err(PyIndexError::new_err("Index out of bounds")), + } + } +} + +#[test] +fn test_option_list_get() { + // Regression test for #798 + let gil = Python::acquire_gil(); + let py = gil.python(); + + let list = PyCell::new( + py, + OptionList { + items: vec![Some(1), None], + }, + ) + .unwrap(); + + py_assert!(py, list, "list[0] == 1"); + py_assert!(py, list, "list[1] == None"); + py_expect_exception!(py, list, "list[2]", PyIndexError); +} diff --git a/tests/ui/abi3_nativetype_inheritance.stderr b/tests/ui/abi3_nativetype_inheritance.stderr index 762250a4..360a0a45 100644 --- a/tests/ui/abi3_nativetype_inheritance.stderr +++ b/tests/ui/abi3_nativetype_inheritance.stderr @@ -15,7 +15,7 @@ error[E0277]: the trait bound `PyDict: PyClass` is not satisfied | = note: required because of the requirements on the impl of `PyClassBaseType` for `PyDict` note: required by a bound in `ThreadCheckerInherited` - --> src/class/impl_.rs + --> src/impl_/pyclass.rs | | pub struct ThreadCheckerInherited(PhantomData, U::ThreadChecker); | ^^^^^^^^^^^^^^^ required by this bound in `ThreadCheckerInherited` diff --git a/tests/ui/not_send.stderr b/tests/ui/not_send.stderr index 166a3d6f..d4a0d4d4 100644 --- a/tests/ui/not_send.stderr +++ b/tests/ui/not_send.stderr @@ -6,9 +6,9 @@ error[E0277]: `*mut pyo3::Python<'static>` cannot be shared between threads safe | = help: within `pyo3::Python<'_>`, the trait `Sync` is not implemented for `*mut pyo3::Python<'static>` = note: required because it appears within the type `PhantomData<*mut pyo3::Python<'static>>` - = note: required because it appears within the type `pyo3::impl_::not_send::NotSend` - = note: required because it appears within the type `(&GILGuard, pyo3::impl_::not_send::NotSend)` - = note: required because it appears within the type `PhantomData<(&GILGuard, pyo3::impl_::not_send::NotSend)>` + = note: required because it appears within the type `impl_::not_send::NotSend` + = note: required because it appears within the type `(&GILGuard, impl_::not_send::NotSend)` + = note: required because it appears within the type `PhantomData<(&GILGuard, impl_::not_send::NotSend)>` = note: required because it appears within the type `pyo3::Python<'_>` = note: required because of the requirements on the impl of `Send` for `&pyo3::Python<'_>` = note: required because it appears within the type `[closure@$DIR/tests/ui/not_send.rs:4:22: 4:38]` diff --git a/tests/ui/pyclass_send.stderr b/tests/ui/pyclass_send.stderr index 531a4e9d..287430ac 100644 --- a/tests/ui/pyclass_send.stderr +++ b/tests/ui/pyclass_send.stderr @@ -11,7 +11,7 @@ note: required because it appears within the type `NotThreadSafe` 5 | struct NotThreadSafe { | ^^^^^^^^^^^^^ note: required by a bound in `ThreadCheckerStub` - --> src/class/impl_.rs + --> src/impl_/pyclass.rs | | pub struct ThreadCheckerStub(PhantomData); | ^^^^ required by this bound in `ThreadCheckerStub`