pymethods: make inventory optional

This commit is contained in:
David Hewitt 2021-03-02 22:34:25 +00:00
parent 525358a33c
commit 977735db20
16 changed files with 353 additions and 160 deletions

View File

@ -88,7 +88,7 @@ jobs:
id: settings id: settings
shell: bash shell: bash
run: | run: |
echo "::set-output name=all_additive_features::macros num-bigint num-complex hashbrown serde" echo "::set-output name=all_additive_features::macros num-bigint num-complex hashbrown serde multiple-pymethods"
- name: Build docs - name: Build docs
run: cargo doc --no-default-features --features "${{ steps.settings.outputs.all_additive_features }}" run: cargo doc --no-default-features --features "${{ steps.settings.outputs.all_additive_features }}"
@ -155,16 +155,15 @@ jobs:
toolchain: nightly toolchain: nightly
override: true override: true
profile: minimal profile: minimal
- uses: actions-rs/cargo@v1 - run: cargo test --no-default-features --no-fail-fast
with: - run: cargo test --no-default-features --no-fail-fast --features "macros num-bigint num-complex hashbrown serde multiple-pymethods"
command: test
args: --features "num-bigint num-complex hashbrown serde" --no-fail-fast
env:
CARGO_INCREMENTAL: 0
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
RUSTDOCFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
- uses: actions-rs/grcov@v0.1 - uses: actions-rs/grcov@v0.1
id: coverage id: coverage
- uses: codecov/codecov-action@v1 - uses: codecov/codecov-action@v1
with: with:
file: ${{ steps.coverage.outputs.report }} file: ${{ steps.coverage.outputs.report }}
env:
CARGO_TERM_VERBOSE: true
CARGO_INCREMENTAL: 0
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
RUSTDOCFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"

View File

@ -43,7 +43,10 @@ serde_json = "1.0.61"
default = ["macros"] default = ["macros"]
# Enables macros: #[pyclass], #[pymodule], #[pyfunction] etc. # Enables macros: #[pyclass], #[pymodule], #[pyfunction] etc.
macros = ["pyo3-macros", "indoc", "inventory", "paste", "unindent"] macros = ["pyo3-macros", "indoc", "paste", "unindent"]
# Enables multiple #[pymethods] per #[pyclass]
multiple-pymethods = ["inventory"]
# Use this feature when building an extension module. # Use this feature when building an extension module.
# It tells the linker to keep the python symbols unresolved, # It tells the linker to keep the python symbols unresolved,

View File

@ -332,7 +332,7 @@ impl SubClass {
PyO3 supports two ways to add properties to your `#[pyclass]`: PyO3 supports two ways to add properties to your `#[pyclass]`:
- For simple fields with no side effects, a `#[pyo3(get, set)]` attribute can be added directly to the field definition in the `#[pyclass]`. - For simple fields with no side effects, a `#[pyo3(get, set)]` attribute can be added directly to the field definition in the `#[pyclass]`.
- For properties which require computation you can define `#[getter]` and `#[setter]` functions in the `#[pymethods]` block. - For properties which require computation you can define `#[getter]` and `#[setter]` functions in the [`#[pymethods]`](#instance-methods) block.
We'll cover each of these in the following sections. We'll cover each of these in the following sections.
@ -711,6 +711,8 @@ See [inventory: how it works](https://github.com/dtolnay/inventory#how-it-works)
Specifically, the following implementation is generated: Specifically, the following implementation is generated:
```rust ```rust
# #[cfg(not(feature = "multiple-pymethods"))]
# { // The implementation differs slightly with the multiple-pymethods feature
use pyo3::prelude::*; use pyo3::prelude::*;
/// Class for demonstration /// Class for demonstration
@ -754,31 +756,14 @@ impl pyo3::IntoPy<PyObject> for MyClass {
} }
} }
pub struct Pyo3MethodsInventoryForMyClass {
methods: Vec<pyo3::class::PyMethodDefType>,
}
impl pyo3::class::methods::PyMethodsInventory for Pyo3MethodsInventoryForMyClass {
fn new(methods: Vec<pyo3::class::PyMethodDefType>) -> Self {
Self { methods }
}
fn get(&'static self) -> &'static [pyo3::class::PyMethodDefType] {
&self.methods
}
}
impl pyo3::class::methods::HasMethodsInventory for MyClass {
type Methods = Pyo3MethodsInventoryForMyClass;
}
pyo3::inventory::collect!(Pyo3MethodsInventoryForMyClass);
impl pyo3::class::impl_::PyClassImpl for MyClass { impl pyo3::class::impl_::PyClassImpl for MyClass {
type ThreadChecker = pyo3::class::impl_::ThreadCheckerStub<MyClass>; type ThreadChecker = pyo3::class::impl_::ThreadCheckerStub<MyClass>;
fn for_each_method_def(visitor: impl FnMut(&pyo3::class::PyMethodDefType)) { fn for_each_method_def(visitor: impl FnMut(&pyo3::class::PyMethodDefType)) {
use pyo3::class::impl_::*; use pyo3::class::impl_::*;
let collector = PyClassImplCollector::<MyClass>::new(); let collector = PyClassImplCollector::<MyClass>::new();
pyo3::inventory::iter::<<MyClass as pyo3::class::methods::HasMethodsInventory>::Methods> collector.py_methods().iter()
.into_iter() .chain(collector.py_class_descriptors())
.flat_map(pyo3::class::methods::PyMethodsInventory::get)
.chain(collector.object_protocol_methods()) .chain(collector.object_protocol_methods())
.chain(collector.async_protocol_methods()) .chain(collector.async_protocol_methods())
.chain(collector.context_protocol_methods()) .chain(collector.context_protocol_methods())
@ -824,6 +809,7 @@ impl pyo3::class::impl_::PyClassImpl for MyClass {
# let py = gil.python(); # let py = gil.python();
# let cls = py.get_type::<MyClass>(); # let cls = py.get_type::<MyClass>();
# pyo3::py_run!(py, cls, "assert cls.__name__ == 'MyClass'") # pyo3::py_run!(py, cls, "assert cls.__name__ == 'MyClass'")
# }
``` ```

View File

@ -24,6 +24,6 @@ pub use from_pyobject::build_derive_from_pyobject;
pub use module::{add_fn_to_module, process_functions_in_module, py_init}; pub use module::{add_fn_to_module, process_functions_in_module, py_init};
pub use pyclass::{build_py_class, PyClassArgs}; pub use pyclass::{build_py_class, PyClassArgs};
pub use pyfunction::{build_py_function, PyFunctionAttr}; pub use pyfunction::{build_py_function, PyFunctionAttr};
pub use pyimpl::build_py_methods; pub use pyimpl::{build_py_methods, PyClassMethodsType};
pub use pyproto::build_py_proto; pub use pyproto::build_py_proto;
pub use utils::get_doc; pub use utils::get_doc;

View File

@ -1,6 +1,7 @@
// Copyright (c) 2017-present PyO3 Project and Contributors // Copyright (c) 2017-present PyO3 Project and Contributors
use crate::method::{FnType, SelfType}; use crate::method::{FnType, SelfType};
use crate::pyimpl::PyClassMethodsType;
use crate::pymethod::{ use crate::pymethod::{
impl_py_getter_def, impl_py_setter_def, impl_wrap_getter, impl_wrap_setter, PropertyType, impl_py_getter_def, impl_py_setter_def, impl_wrap_getter, impl_wrap_setter, PropertyType,
}; };
@ -154,7 +155,11 @@ impl PyClassArgs {
} }
} }
pub fn build_py_class(class: &mut syn::ItemStruct, attr: &PyClassArgs) -> syn::Result<TokenStream> { pub fn build_py_class(
class: &mut syn::ItemStruct,
attr: &PyClassArgs,
methods_type: PyClassMethodsType,
) -> syn::Result<TokenStream> {
let text_signature = utils::parse_text_signature_attrs( let text_signature = utils::parse_text_signature_attrs(
&mut class.attrs, &mut class.attrs,
&get_class_python_name(&class.ident, attr), &get_class_python_name(&class.ident, attr),
@ -178,7 +183,7 @@ pub fn build_py_class(class: &mut syn::ItemStruct, attr: &PyClassArgs) -> syn::R
bail_spanned!(class.fields.span() => "#[pyclass] can only be used with C-style structs"); bail_spanned!(class.fields.span() => "#[pyclass] can only be used with C-style structs");
} }
impl_class(&class.ident, &attr, doc, descriptors) impl_class(&class.ident, &attr, doc, descriptors, methods_type)
} }
/// Parses `#[pyo3(get, set)]` /// Parses `#[pyo3(get, set)]`
@ -210,7 +215,7 @@ fn parse_descriptors(item: &mut syn::Field) -> syn::Result<Vec<FnType>> {
Ok(descs) Ok(descs)
} }
/// To allow multiple #[pymethods]/#[pyproto] block, we define inventory types. /// To allow multiple #[pymethods] block, we define inventory types.
fn impl_methods_inventory(cls: &syn::Ident) -> TokenStream { fn impl_methods_inventory(cls: &syn::Ident) -> TokenStream {
// Try to build a unique type for better error messages // Try to build a unique type for better error messages
let name = format!("Pyo3MethodsInventoryFor{}", cls.unraw()); let name = format!("Pyo3MethodsInventoryFor{}", cls.unraw());
@ -221,7 +226,7 @@ fn impl_methods_inventory(cls: &syn::Ident) -> TokenStream {
pub struct #inventory_cls { pub struct #inventory_cls {
methods: Vec<pyo3::class::PyMethodDefType>, methods: Vec<pyo3::class::PyMethodDefType>,
} }
impl pyo3::class::methods::PyMethodsInventory for #inventory_cls { impl pyo3::class::impl_::PyMethodsInventory for #inventory_cls {
fn new(methods: Vec<pyo3::class::PyMethodDefType>) -> Self { fn new(methods: Vec<pyo3::class::PyMethodDefType>) -> Self {
Self { methods } Self { methods }
} }
@ -230,7 +235,7 @@ fn impl_methods_inventory(cls: &syn::Ident) -> TokenStream {
} }
} }
impl pyo3::class::methods::HasMethodsInventory for #cls { impl pyo3::class::impl_::HasMethodsInventory for #cls {
type Methods = #inventory_cls; type Methods = #inventory_cls;
} }
@ -247,6 +252,7 @@ fn impl_class(
attr: &PyClassArgs, attr: &PyClassArgs,
doc: syn::LitStr, doc: syn::LitStr,
descriptors: Vec<(syn::Field, Vec<FnType>)>, descriptors: Vec<(syn::Field, Vec<FnType>)>,
methods_type: PyClassMethodsType,
) -> syn::Result<TokenStream> { ) -> syn::Result<TokenStream> {
let cls_name = get_class_python_name(cls, attr).to_string(); let cls_name = get_class_python_name(cls, attr).to_string();
@ -338,7 +344,17 @@ fn impl_class(
quote! {} quote! {}
}; };
let impl_inventory = impl_methods_inventory(&cls); let (impl_inventory, iter_py_methods) = match methods_type {
PyClassMethodsType::Specialization => (None, quote! { collector.py_methods().iter() }),
PyClassMethodsType::Inventory => (
Some(impl_methods_inventory(&cls)),
quote! {
pyo3::inventory::iter::<<Self as pyo3::class::impl_::HasMethodsInventory>::Methods>
.into_iter()
.flat_map(pyo3::class::impl_::PyMethodsInventory::get)
},
),
};
let base = &attr.base; let base = &attr.base;
let flags = &attr.flags; let flags = &attr.flags;
@ -429,9 +445,8 @@ fn impl_class(
fn for_each_method_def(visitor: impl FnMut(&pyo3::class::PyMethodDefType)) { fn for_each_method_def(visitor: impl FnMut(&pyo3::class::PyMethodDefType)) {
use pyo3::class::impl_::*; use pyo3::class::impl_::*;
let collector = PyClassImplCollector::<Self>::new(); let collector = PyClassImplCollector::<Self>::new();
pyo3::inventory::iter::<<Self as pyo3::class::methods::HasMethodsInventory>::Methods> #iter_py_methods
.into_iter() .chain(collector.py_class_descriptors())
.flat_map(pyo3::class::methods::PyMethodsInventory::get)
.chain(collector.object_protocol_methods()) .chain(collector.object_protocol_methods())
.chain(collector.async_protocol_methods()) .chain(collector.async_protocol_methods())
.chain(collector.context_protocol_methods()) .chain(collector.context_protocol_methods())
@ -513,10 +528,12 @@ fn impl_descriptors(
.collect::<syn::Result<_>>()?; .collect::<syn::Result<_>>()?;
Ok(quote! { Ok(quote! {
pyo3::inventory::submit! { impl pyo3::class::impl_::PyClassDescriptors<#cls>
#![crate = pyo3] { for pyo3::class::impl_::PyClassImplCollector<#cls>
type Inventory = <#cls as pyo3::class::methods::HasMethodsInventory>::Methods; {
<Inventory as pyo3::class::methods::PyMethodsInventory>::new(vec![#(#py_methods),*]) fn py_class_descriptors(self) -> &'static [pyo3::class::methods::PyMethodDefType] {
static METHODS: &[pyo3::class::methods::PyMethodDefType] = &[#(#py_methods),*];
METHODS
} }
} }
}) })

View File

@ -6,7 +6,16 @@ use pymethod::GeneratedPyMethod;
use quote::quote; use quote::quote;
use syn::spanned::Spanned; use syn::spanned::Spanned;
pub fn build_py_methods(ast: &mut syn::ItemImpl) -> syn::Result<TokenStream> { /// The mechanism used to collect `#[pymethods]` into the type object
pub enum PyClassMethodsType {
Specialization,
Inventory,
}
pub fn build_py_methods(
ast: &mut syn::ItemImpl,
methods_type: PyClassMethodsType,
) -> syn::Result<TokenStream> {
if let Some((_, path, _)) = &ast.trait_ { if let Some((_, path, _)) = &ast.trait_ {
bail_spanned!(path.span() => "#[pymethods] cannot be used on trait impl blocks"); bail_spanned!(path.span() => "#[pymethods] cannot be used on trait impl blocks");
} else if ast.generics != Default::default() { } else if ast.generics != Default::default() {
@ -15,11 +24,15 @@ pub fn build_py_methods(ast: &mut syn::ItemImpl) -> syn::Result<TokenStream> {
"#[pymethods] cannot be used with lifetime parameters or generics" "#[pymethods] cannot be used with lifetime parameters or generics"
); );
} else { } else {
impl_methods(&ast.self_ty, &mut ast.items) impl_methods(&ast.self_ty, &mut ast.items, methods_type)
} }
} }
pub fn impl_methods(ty: &syn::Type, impls: &mut Vec<syn::ImplItem>) -> syn::Result<TokenStream> { pub fn impl_methods(
ty: &syn::Type,
impls: &mut Vec<syn::ImplItem>,
methods_type: PyClassMethodsType,
) -> syn::Result<TokenStream> {
let mut new_impls = Vec::new(); let mut new_impls = Vec::new();
let mut call_impls = Vec::new(); let mut call_impls = Vec::new();
let mut methods = Vec::new(); let mut methods = Vec::new();
@ -51,18 +64,46 @@ pub fn impl_methods(ty: &syn::Type, impls: &mut Vec<syn::ImplItem>) -> syn::Resu
} }
} }
let methods_registration = match methods_type {
PyClassMethodsType::Specialization => impl_py_methods(ty, methods),
PyClassMethodsType::Inventory => submit_methods_inventory(ty, methods),
};
Ok(quote! { Ok(quote! {
#(#new_impls)* #(#new_impls)*
#(#call_impls)* #(#call_impls)*
#methods_registration
})
}
fn impl_py_methods(ty: &syn::Type, methods: Vec<TokenStream>) -> TokenStream {
quote! {
impl pyo3::class::impl_::PyMethods<#ty>
for pyo3::class::impl_::PyClassImplCollector<#ty>
{
fn py_methods(self) -> &'static [pyo3::class::methods::PyMethodDefType] {
static METHODS: &[pyo3::class::methods::PyMethodDefType] = &[#(#methods),*];
METHODS
}
}
}
}
fn submit_methods_inventory(ty: &syn::Type, methods: Vec<TokenStream>) -> TokenStream {
if methods.is_empty() {
return TokenStream::default();
}
quote! {
pyo3::inventory::submit! { pyo3::inventory::submit! {
#![crate = pyo3] { #![crate = pyo3] {
type Inventory = <#ty as pyo3::class::methods::HasMethodsInventory>::Methods; type Inventory = <#ty as pyo3::class::impl_::HasMethodsInventory>::Methods;
<Inventory as pyo3::class::methods::PyMethodsInventory>::new(vec![#(#methods),*]) <Inventory as pyo3::class::impl_::PyMethodsInventory>::new(vec![#(#methods),*])
}
} }
} }
})
} }
fn get_cfg_attributes(attrs: &[syn::Attribute]) -> Vec<&syn::Attribute> { fn get_cfg_attributes(attrs: &[syn::Attribute]) -> Vec<&syn::Attribute> {

View File

@ -689,7 +689,10 @@ pub fn impl_py_method_class_attribute(spec: &FnSpec<'_>, wrapper: &TokenStream)
pyo3::class::PyMethodDefType::ClassAttribute({ pyo3::class::PyMethodDefType::ClassAttribute({
#wrapper #wrapper
pyo3::class::PyClassAttributeDef::new(concat!(stringify!(#python_name), "\0"), __wrap) pyo3::class::PyClassAttributeDef::new(
concat!(stringify!(#python_name), "\0"),
pyo3::class::methods::PyClassAttributeFactory(__wrap)
)
}) })
} }
} }
@ -700,7 +703,10 @@ pub fn impl_py_const_class_attribute(spec: &ConstSpec, wrapper: &TokenStream) ->
pyo3::class::PyMethodDefType::ClassAttribute({ pyo3::class::PyMethodDefType::ClassAttribute({
#wrapper #wrapper
pyo3::class::PyClassAttributeDef::new(concat!(stringify!(#python_name), "\0"), __wrap) pyo3::class::PyClassAttributeDef::new(
concat!(stringify!(#python_name), "\0"),
pyo3::class::methods::PyClassAttributeFactory(__wrap)
)
}) })
} }
} }
@ -726,7 +732,11 @@ pub(crate) fn impl_py_setter_def(
pyo3::class::PyMethodDefType::Setter({ pyo3::class::PyMethodDefType::Setter({
#wrapper #wrapper
pyo3::class::PySetterDef::new(concat!(stringify!(#python_name), "\0"), __wrap, #doc) pyo3::class::PySetterDef::new(
concat!(stringify!(#python_name), "\0"),
pyo3::class::methods::PySetter(__wrap),
#doc
)
}) })
} }
} }
@ -740,7 +750,11 @@ pub(crate) fn impl_py_getter_def(
pyo3::class::PyMethodDefType::Getter({ pyo3::class::PyMethodDefType::Getter({
#wrapper #wrapper
pyo3::class::PyGetterDef::new(concat!(stringify!(#python_name), "\0"), __wrap, #doc) pyo3::class::PyGetterDef::new(
concat!(stringify!(#python_name), "\0"),
pyo3::class::methods::PyGetter(__wrap),
#doc
)
}) })
} }
} }

View File

@ -7,7 +7,8 @@ extern crate proc_macro;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use pyo3_macros_backend::{ use pyo3_macros_backend::{
build_derive_from_pyobject, build_py_class, build_py_function, build_py_methods, build_derive_from_pyobject, build_py_class, build_py_function, build_py_methods,
build_py_proto, get_doc, process_functions_in_module, py_init, PyClassArgs, PyFunctionAttr, build_py_proto, get_doc, process_functions_in_module, py_init, PyClassArgs, PyClassMethodsType,
PyFunctionAttr,
}; };
use quote::quote; use quote::quote;
use syn::parse_macro_input; use syn::parse_macro_input;
@ -56,27 +57,22 @@ pub fn pyproto(_: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream { pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream {
let mut ast = parse_macro_input!(input as syn::ItemStruct); pyclass_impl(attr, input, PyClassMethodsType::Specialization)
let args = parse_macro_input!(attr as PyClassArgs); }
let expanded = build_py_class(&mut ast, &args).unwrap_or_else(|e| e.to_compile_error());
quote!( #[proc_macro_attribute]
#ast pub fn pyclass_with_inventory(attr: TokenStream, input: TokenStream) -> TokenStream {
#expanded pyclass_impl(attr, input, PyClassMethodsType::Inventory)
)
.into()
} }
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn pymethods(_: TokenStream, input: TokenStream) -> TokenStream { pub fn pymethods(_: TokenStream, input: TokenStream) -> TokenStream {
let mut ast = parse_macro_input!(input as syn::ItemImpl); pymethods_impl(input, PyClassMethodsType::Specialization)
let expanded = build_py_methods(&mut ast).unwrap_or_else(|e| e.to_compile_error()); }
quote!( #[proc_macro_attribute]
#ast pub fn pymethods_with_inventory(_: TokenStream, input: TokenStream) -> TokenStream {
#expanded pymethods_impl(input, PyClassMethodsType::Inventory)
)
.into()
} }
#[proc_macro_attribute] #[proc_macro_attribute]
@ -102,3 +98,32 @@ pub fn derive_from_py_object(item: TokenStream) -> TokenStream {
) )
.into() .into()
} }
fn pyclass_impl(
attr: TokenStream,
input: TokenStream,
methods_type: PyClassMethodsType,
) -> TokenStream {
let mut ast = parse_macro_input!(input as syn::ItemStruct);
let args = parse_macro_input!(attr as PyClassArgs);
let expanded =
build_py_class(&mut ast, &args, methods_type).unwrap_or_else(|e| e.to_compile_error());
quote!(
#ast
#expanded
)
.into()
}
fn pymethods_impl(input: TokenStream, methods_type: PyClassMethodsType) -> TokenStream {
let mut ast = parse_macro_input!(input as syn::ItemImpl);
let expanded =
build_py_methods(&mut ast, methods_type).unwrap_or_else(|e| e.to_compile_error());
quote!(
#ast
#expanded
)
.into()
}

View File

@ -76,6 +76,50 @@ impl<T> PyClassCallImpl<T> for &'_ PyClassImplCollector<T> {
} }
} }
// 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<T> {
fn $function_name(self) -> &'static [PyMethodDefType];
}
impl<T> $name<T> for &'_ PyClassImplCollector<T> {
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(all(feature = "macros", feature = "multiple-pymethods"))]
pub trait PyMethodsInventory: inventory::Collect {
/// Create a new instance
fn new(methods: Vec<PyMethodDefType>) -> Self;
/// Returns the methods for a single `#[pymethods] impl` block
fn get(&'static self) -> &'static [PyMethodDefType];
}
/// Implemented for `#[pyclass]` in our proc macro code.
/// Indicates that the pyclass has its own method storage.
#[cfg(all(feature = "macros", feature = "multiple-pymethods"))]
pub trait HasMethodsInventory {
type Methods: PyMethodsInventory;
}
// 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 // All traits describing slots, as well as the fallback implementations for unimplemented protos
// //
// Protos which are implemented use dtolnay specialization to implement for PyClassImplCollector<T>. // Protos which are implemented use dtolnay specialization to implement for PyClassImplCollector<T>.
@ -106,20 +150,6 @@ slots_trait!(PyAsyncProtocolSlots, async_protocol_slots);
slots_trait!(PySequenceProtocolSlots, sequence_protocol_slots); slots_trait!(PySequenceProtocolSlots, sequence_protocol_slots);
slots_trait!(PyBufferProtocolSlots, buffer_protocol_slots); slots_trait!(PyBufferProtocolSlots, buffer_protocol_slots);
macro_rules! methods_trait {
($name:ident, $function_name: ident) => {
pub trait $name<T> {
fn $function_name(self) -> &'static [PyMethodDefType];
}
impl<T> $name<T> for &'_ PyClassImplCollector<T> {
fn $function_name(self) -> &'static [PyMethodDefType] {
&[]
}
}
};
}
methods_trait!(PyObjectProtocolMethods, object_protocol_methods); methods_trait!(PyObjectProtocolMethods, object_protocol_methods);
methods_trait!(PyAsyncProtocolMethods, async_protocol_methods); methods_trait!(PyAsyncProtocolMethods, async_protocol_methods);
methods_trait!(PyContextProtocolMethods, context_protocol_methods); methods_trait!(PyContextProtocolMethods, context_protocol_methods);

View File

@ -1,7 +1,8 @@
// Copyright (c) 2017-present PyO3 Project and Contributors // Copyright (c) 2017-present PyO3 Project and Contributors
use crate::internal_tricks::{extract_cstr_or_leak_cstring, NulByteInString};
use crate::{ffi, PyObject, Python}; use crate::{ffi, PyObject, Python};
use std::ffi::{CStr, CString}; use std::ffi::CStr;
use std::fmt; use std::fmt;
use std::os::raw::c_int; use std::os::raw::c_int;
@ -29,16 +30,22 @@ pub enum PyMethodType {
PyCFunctionWithKeywords(PyCFunctionWithKeywords), PyCFunctionWithKeywords(PyCFunctionWithKeywords),
} }
// These two newtype structs serve no purpose other than wrapping the raw ffi types (which are // These newtype structs serve no purpose other than wrapping which are function pointers - because
// function pointers) - because function pointers aren't allowed in const fn, but types wrapping // function pointers aren't allowed in const fn, but types wrapping them are!
// them are!
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct PyCFunction(pub ffi::PyCFunction); pub struct PyCFunction(pub ffi::PyCFunction);
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct PyCFunctionWithKeywords(pub ffi::PyCFunctionWithKeywords); pub struct PyCFunctionWithKeywords(pub ffi::PyCFunctionWithKeywords);
#[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 // 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. // until `CStr::from_bytes_with_nul_unchecked` is const fn.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct PyMethodDef { pub struct PyMethodDef {
pub(crate) ml_name: &'static str, pub(crate) ml_name: &'static str,
@ -49,22 +56,22 @@ pub struct PyMethodDef {
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct PyClassAttributeDef { pub struct PyClassAttributeDef {
pub(crate) name: &'static CStr, pub(crate) name: &'static str,
pub(crate) meth: for<'p> fn(Python<'p>) -> PyObject, pub(crate) meth: PyClassAttributeFactory,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct PyGetterDef { pub struct PyGetterDef {
pub(crate) name: &'static CStr, pub(crate) name: &'static str,
pub(crate) meth: ffi::getter, pub(crate) meth: PyGetter,
doc: &'static CStr, doc: &'static str,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct PySetterDef { pub struct PySetterDef {
pub(crate) name: &'static CStr, pub(crate) name: &'static str,
pub(crate) meth: ffi::setter, pub(crate) meth: PySetter,
doc: &'static CStr, doc: &'static str,
} }
unsafe impl Sync for PyMethodDef {} unsafe impl Sync for PyMethodDef {}
@ -117,11 +124,8 @@ impl PyMethodDef {
impl PyClassAttributeDef { impl PyClassAttributeDef {
/// Define a class attribute. /// Define a class attribute.
pub fn new(name: &'static str, meth: for<'p> fn(Python<'p>) -> PyObject) -> Self { pub const fn new(name: &'static str, meth: PyClassAttributeFactory) -> Self {
Self { Self { name, meth }
name: get_name(name).unwrap(),
meth,
}
} }
} }
@ -137,73 +141,48 @@ impl fmt::Debug for PyClassAttributeDef {
impl PyGetterDef { impl PyGetterDef {
/// Define a getter. /// Define a getter.
pub fn new(name: &'static str, getter: ffi::getter, doc: &'static str) -> Self { pub const fn new(name: &'static str, getter: PyGetter, doc: &'static str) -> Self {
Self { Self {
name: get_name(name).unwrap(), name,
meth: getter, meth: getter,
doc: get_doc(doc).unwrap(), doc,
} }
} }
/// Copy descriptor information to `ffi::PyGetSetDef` /// Copy descriptor information to `ffi::PyGetSetDef`
pub fn copy_to(&self, dst: &mut ffi::PyGetSetDef) { pub fn copy_to(&self, dst: &mut ffi::PyGetSetDef) {
if dst.name.is_null() { if dst.name.is_null() {
dst.name = self.name.as_ptr() as _; dst.name = get_name(self.name).unwrap().as_ptr() as _;
} }
if dst.doc.is_null() { if dst.doc.is_null() {
dst.doc = self.doc.as_ptr() as _; dst.doc = get_doc(self.doc).unwrap().as_ptr() as _;
} }
dst.get = Some(self.meth); dst.get = Some(self.meth.0);
} }
} }
impl PySetterDef { impl PySetterDef {
/// Define a setter. /// Define a setter.
pub fn new(name: &'static str, setter: ffi::setter, doc: &'static str) -> Self { pub const fn new(name: &'static str, setter: PySetter, doc: &'static str) -> Self {
Self { Self {
name: get_name(name).unwrap(), name,
meth: setter, meth: setter,
doc: get_doc(doc).unwrap(), doc,
} }
} }
/// Copy descriptor information to `ffi::PyGetSetDef` /// Copy descriptor information to `ffi::PyGetSetDef`
pub fn copy_to(&self, dst: &mut ffi::PyGetSetDef) { pub fn copy_to(&self, dst: &mut ffi::PyGetSetDef) {
if dst.name.is_null() { if dst.name.is_null() {
dst.name = self.name.as_ptr() as _; dst.name = get_name(self.name).unwrap().as_ptr() as _;
} }
if dst.doc.is_null() { if dst.doc.is_null() {
dst.doc = self.doc.as_ptr() as _; dst.doc = get_doc(self.doc).unwrap().as_ptr() as _;
} }
dst.set = Some(self.meth); dst.set = Some(self.meth.0);
} }
} }
/// Implementation detail. Only to be used through our proc macro code.
/// Method storage for `#[pyclass]`.
/// Allows arbitrary `#[pymethod]/#[pyproto]` blocks to submit their methods,
/// which are eventually collected by `#[pyclass]`.
#[doc(hidden)]
#[cfg(feature = "macros")]
pub trait PyMethodsInventory: inventory::Collect {
/// Create a new instance
fn new(methods: Vec<PyMethodDefType>) -> Self;
/// Returns the methods for a single `#[pymethods] impl` block
fn get(&'static self) -> &'static [PyMethodDefType];
}
/// Implemented for `#[pyclass]` in our proc macro code.
/// Indicates that the pyclass has its own method storage.
#[doc(hidden)]
#[cfg(feature = "macros")]
pub trait HasMethodsInventory {
type Methods: PyMethodsInventory;
}
#[derive(Debug)]
pub(crate) struct NulByteInString(pub(crate) &'static str);
fn get_name(name: &'static str) -> Result<&'static CStr, NulByteInString> { fn get_name(name: &'static str) -> Result<&'static CStr, NulByteInString> {
extract_cstr_or_leak_cstring(name, "Function name cannot contain NUL byte.") extract_cstr_or_leak_cstring(name, "Function name cannot contain NUL byte.")
} }
@ -211,14 +190,3 @@ fn get_name(name: &'static str) -> Result<&'static CStr, NulByteInString> {
fn get_doc(doc: &'static str) -> Result<&'static CStr, NulByteInString> { fn get_doc(doc: &'static str) -> Result<&'static CStr, NulByteInString> {
extract_cstr_or_leak_cstring(doc, "Document cannot contain NUL byte.") extract_cstr_or_leak_cstring(doc, "Document cannot contain NUL byte.")
} }
fn extract_cstr_or_leak_cstring(
src: &'static str,
err_msg: &'static str,
) -> Result<&'static CStr, NulByteInString> {
CStr::from_bytes_with_nul(src.as_bytes())
.or_else(|_| {
CString::new(src.as_bytes()).map(|c_string| &*Box::leak(c_string.into_boxed_c_str()))
})
.map_err(|_| NulByteInString(err_msg))
}

View File

@ -1,3 +1,4 @@
use std::ffi::{CStr, CString};
use std::marker::PhantomData; use std::marker::PhantomData;
use std::rc::Rc; use std::rc::Rc;
@ -36,3 +37,17 @@ macro_rules! pyo3_exception {
$crate::create_exception_type_object!(pyo3_runtime, $name, $base); $crate::create_exception_type_object!(pyo3_runtime, $name, $base);
}; };
} }
#[derive(Debug)]
pub(crate) struct NulByteInString(pub(crate) &'static str);
pub(crate) fn extract_cstr_or_leak_cstring(
src: &'static str,
err_msg: &'static str,
) -> Result<&'static CStr, NulByteInString> {
CStr::from_bytes_with_nul(src.as_bytes())
.or_else(|_| {
CString::new(src.as_bytes()).map(|c_string| &*Box::leak(c_string.into_boxed_c_str()))
})
.map_err(|_| NulByteInString(err_msg))
}

View File

@ -167,11 +167,13 @@ pub use crate::types::PyAny;
#[doc(hidden)] #[doc(hidden)]
pub use { pub use {
indoc, // Re-exported for py_run indoc, // Re-exported for py_run
inventory, // Re-exported for pymethods
paste, // Re-exported for wrap_function paste, // Re-exported for wrap_function
unindent, // Re-exported for py_run unindent, // Re-exported for py_run
}; };
#[cfg(all(feature = "macros", feature = "multiple-pymethods"))]
pub use inventory;
#[macro_use] #[macro_use]
mod internal_tricks; mod internal_tricks;
@ -216,7 +218,15 @@ pub mod serde;
pub mod proc_macro { pub mod proc_macro {
pub use pyo3_macros::pymodule; pub use pyo3_macros::pymodule;
/// The proc macro attributes /// The proc macro attributes
pub use pyo3_macros::{pyclass, pyfunction, pymethods, pyproto}; pub use pyo3_macros::{pyfunction, pyproto};
#[cfg(not(feature = "multiple-pymethods"))]
pub use pyo3_macros::{pyclass, pymethods};
#[cfg(feature = "multiple-pymethods")]
pub use pyo3_macros::{
pyclass_with_inventory as pyclass, pymethods_with_inventory as pymethods,
};
} }
/// Returns a function that takes a [Python] instance and returns a Python function. /// Returns a function that takes a [Python] instance and returns a Python function.

View File

@ -20,4 +20,4 @@ pub use crate::{FromPyObject, IntoPy, IntoPyPointer, PyTryFrom, PyTryInto, ToPyO
// PyModule is only part of the prelude because we need it for the pymodule function // PyModule is only part of the prelude because we need it for the pymodule function
pub use crate::types::{PyAny, PyModule}; pub use crate::types::{PyAny, PyModule};
#[cfg(feature = "macros")] #[cfg(feature = "macros")]
pub use pyo3_macros::{pyclass, pyfunction, pymethods, pymodule, pyproto, FromPyObject}; pub use {crate::proc_macro::*, pyo3_macros::FromPyObject};

View File

@ -1,6 +1,7 @@
// Copyright (c) 2017-present PyO3 Project and Contributors // Copyright (c) 2017-present PyO3 Project and Contributors
//! Python type object information //! Python type object information
use crate::internal_tricks::extract_cstr_or_leak_cstring;
use crate::once_cell::GILOnceCell; use crate::once_cell::GILOnceCell;
use crate::pyclass::{create_type_object, PyClass}; use crate::pyclass::{create_type_object, PyClass};
use crate::pyclass_init::PyObjectInit; use crate::pyclass_init::PyObjectInit;
@ -197,7 +198,14 @@ impl LazyStaticType {
let mut items = vec![]; let mut items = vec![];
T::for_each_method_def(|def| { T::for_each_method_def(|def| {
if let PyMethodDefType::ClassAttribute(attr) = def { if let PyMethodDefType::ClassAttribute(attr) = def {
items.push((attr.name, (attr.meth)(py))); items.push((
extract_cstr_or_leak_cstring(
attr.name,
"class attribute name cannot contain nul bytes",
)
.unwrap(),
(attr.meth.0)(py),
));
} }
}); });

View File

@ -1,10 +1,10 @@
use crate::derive_utils::PyFunctionArguments;
use crate::exceptions::PyValueError; use crate::exceptions::PyValueError;
use crate::prelude::*; use crate::prelude::*;
use crate::{ use crate::{
class::methods::{self, PyMethodDef}, class::methods::{self, PyMethodDef},
ffi, AsPyPointer, ffi, AsPyPointer,
}; };
use crate::{derive_utils::PyFunctionArguments, methods::NulByteInString};
/// Represents a builtin Python function object. /// Represents a builtin Python function object.
#[repr(transparent)] #[repr(transparent)]
@ -54,7 +54,7 @@ impl PyCFunction {
let (py, module) = py_or_module.into_py_and_maybe_module(); let (py, module) = py_or_module.into_py_and_maybe_module();
let def = method_def let def = method_def
.as_method_def() .as_method_def()
.map_err(|NulByteInString(err)| PyValueError::new_err(err))?; .map_err(|err| PyValueError::new_err(err.0))?;
let (mod_ptr, module_name) = if let Some(m) = module { let (mod_ptr, module_name) = if let Some(m) = module {
let mod_ptr = m.as_ptr(); let mod_ptr = m.as_ptr();
let name = m.name()?.into_py(py); let name = m.name()?.into_py(py);

View File

@ -0,0 +1,77 @@
#![cfg(feature = "multiple-pymethods")]
use pyo3::prelude::*;
use pyo3::type_object::PyTypeObject;
use pyo3::types::PyType;
#[macro_use]
mod common;
#[pyclass]
struct PyClassWithMultiplePyMethods {}
#[pymethods]
impl PyClassWithMultiplePyMethods {
#[new]
fn new() -> Self {
Self {}
}
}
#[pymethods]
impl PyClassWithMultiplePyMethods {
#[call]
fn call(&self) -> &'static str {
"call"
}
}
#[pymethods]
impl PyClassWithMultiplePyMethods {
fn method(&self) -> &'static str {
"method"
}
}
#[pymethods]
impl PyClassWithMultiplePyMethods {
#[classmethod]
fn classmethod(_ty: &PyType) -> &'static str {
"classmethod"
}
}
#[pymethods]
impl PyClassWithMultiplePyMethods {
#[staticmethod]
fn staticmethod() -> &'static str {
"staticmethod"
}
}
#[pymethods]
impl PyClassWithMultiplePyMethods {
#[classattr]
fn class_attribute() -> &'static str {
"class_attribute"
}
}
#[pymethods]
impl PyClassWithMultiplePyMethods {
#[classattr]
const CLASS_ATTRIBUTE: &'static str = "CLASS_ATTRIBUTE";
}
#[test]
fn test_class_with_multiple_pymethods() {
Python::with_gil(|py| {
let cls = PyClassWithMultiplePyMethods::type_object(py);
py_assert!(py, cls, "cls()() == 'call'");
py_assert!(py, cls, "cls().method() == 'method'");
py_assert!(py, cls, "cls.classmethod() == 'classmethod'");
py_assert!(py, cls, "cls.staticmethod() == 'staticmethod'");
py_assert!(py, cls, "cls.class_attribute == 'class_attribute'");
py_assert!(py, cls, "cls.CLASS_ATTRIBUTE == 'CLASS_ATTRIBUTE'");
})
}