Skip to content

Commit b4f247f

Browse files
committed
Introduce PyProtoMethodsImpl and remove *ProtocolImpl::methods
1 parent 818ebf7 commit b4f247f

File tree

11 files changed

+191
-625
lines changed

11 files changed

+191
-625
lines changed

guide/src/class.md

+88-71
Original file line numberDiff line numberDiff line change
@@ -17,76 +17,7 @@ struct MyClass {
1717
The above example generates implementations for [`PyTypeInfo`], [`PyTypeObject`],
1818
and [`PyClass`] for `MyClass`.
1919

20-
Specifically, the following implementation is generated:
21-
22-
```rust
23-
use pyo3::prelude::*;
24-
25-
/// Class for demonstration
26-
struct MyClass {
27-
num: i32,
28-
debug: bool,
29-
}
30-
31-
impl pyo3::pyclass::PyClassAlloc for MyClass {}
32-
33-
unsafe impl pyo3::PyTypeInfo for MyClass {
34-
type Type = MyClass;
35-
type BaseType = PyAny;
36-
type BaseLayout = pyo3::pycell::PyCellBase<PyAny>;
37-
type Layout = PyCell<Self>;
38-
type Initializer = PyClassInitializer<Self>;
39-
type AsRefTarget = PyCell<Self>;
40-
41-
const NAME: &'static str = "MyClass";
42-
const MODULE: Option<&'static str> = None;
43-
const DESCRIPTION: &'static str = "Class for demonstration";
44-
const FLAGS: usize = 0;
45-
46-
#[inline]
47-
fn type_object() -> &'static pyo3::ffi::PyTypeObject {
48-
use pyo3::type_object::LazyStaticType;
49-
static TYPE_OBJECT: LazyStaticType = LazyStaticType::new();
50-
TYPE_OBJECT.get_or_init::<Self>()
51-
}
52-
}
53-
54-
impl pyo3::pyclass::PyClass for MyClass {
55-
type Dict = pyo3::pyclass_slots::PyClassDummySlot;
56-
type WeakRef = pyo3::pyclass_slots::PyClassDummySlot;
57-
type BaseNativeType = PyAny;
58-
}
59-
60-
impl pyo3::IntoPy<PyObject> for MyClass {
61-
fn into_py(self, py: pyo3::Python) -> pyo3::PyObject {
62-
pyo3::IntoPy::into_py(pyo3::Py::new(py, self).unwrap(), py)
63-
}
64-
}
65-
66-
pub struct Pyo3MethodsInventoryForMyClass {
67-
methods: &'static [pyo3::class::PyMethodDefType],
68-
}
69-
70-
impl pyo3::class::methods::PyMethodsInventory for Pyo3MethodsInventoryForMyClass {
71-
fn new(methods: &'static [pyo3::class::PyMethodDefType]) -> Self {
72-
Self { methods }
73-
}
74-
75-
fn get_methods(&self) -> &'static [pyo3::class::PyMethodDefType] {
76-
self.methods
77-
}
78-
}
79-
80-
impl pyo3::class::methods::PyMethodsImpl for MyClass {
81-
type Methods = Pyo3MethodsInventoryForMyClass;
82-
}
83-
84-
pyo3::inventory::collect!(Pyo3MethodsInventoryForMyClass);
85-
# let gil = Python::acquire_gil();
86-
# let py = gil.python();
87-
# let cls = py.get_type::<MyClass>();
88-
# pyo3::py_run!(py, cls, "assert cls.__name__ == 'MyClass'")
89-
```
20+
If you curious what `#[pyclass]` generates, see [How methods are implemented](#how-methods-are-implemented) section.
9021

9122
## Adding the class to a module
9223

@@ -944,7 +875,93 @@ pyclass dependent on whether there is an impl block, we'd need to implement the
944875
`#[pyclass]` and override the implementation in `#[pymethods]`, which is to the best of my knowledge
945876
only possible with the specialization feature, which can't be used on stable.
946877

947-
To escape this we use [inventory](https://github.com/dtolnay/inventory), which allows us to collect `impl`s from arbitrary source code by exploiting some binary trick. See [inventory: how it works](https://github.com/dtolnay/inventory#how-it-works) and `pyo3_derive_backend::py_class::impl_inventory` for more details.
878+
To escape this we use [inventory](https://github.com/dtolnay/inventory),
879+
which allows us to collect `impl`s from arbitrary source code by exploiting some binary trick.
880+
See [inventory: how it works](https://github.com/dtolnay/inventory#how-it-works) and `pyo3_derive_backend::py_class` for more details.
881+
882+
Specifically, the following implementation is generated:
883+
884+
```rust
885+
use pyo3::prelude::*;
886+
887+
/// Class for demonstration
888+
struct MyClass {
889+
num: i32,
890+
debug: bool,
891+
}
892+
893+
impl pyo3::pyclass::PyClassAlloc for MyClass {}
894+
895+
unsafe impl pyo3::PyTypeInfo for MyClass {
896+
type Type = MyClass;
897+
type BaseType = PyAny;
898+
type BaseLayout = pyo3::pycell::PyCellBase<PyAny>;
899+
type Layout = PyCell<Self>;
900+
type Initializer = PyClassInitializer<Self>;
901+
type AsRefTarget = PyCell<Self>;
902+
903+
const NAME: &'static str = "MyClass";
904+
const MODULE: Option<&'static str> = None;
905+
const DESCRIPTION: &'static str = "Class for demonstration";
906+
const FLAGS: usize = 0;
907+
908+
#[inline]
909+
fn type_object() -> &'static pyo3::ffi::PyTypeObject {
910+
use pyo3::type_object::LazyStaticType;
911+
static TYPE_OBJECT: LazyStaticType = LazyStaticType::new();
912+
TYPE_OBJECT.get_or_init::<Self>()
913+
}
914+
}
915+
916+
impl pyo3::pyclass::PyClass for MyClass {
917+
type Dict = pyo3::pyclass_slots::PyClassDummySlot;
918+
type WeakRef = pyo3::pyclass_slots::PyClassDummySlot;
919+
type BaseNativeType = PyAny;
920+
}
921+
922+
impl pyo3::IntoPy<PyObject> for MyClass {
923+
fn into_py(self, py: pyo3::Python) -> pyo3::PyObject {
924+
pyo3::IntoPy::into_py(pyo3::Py::new(py, self).unwrap(), py)
925+
}
926+
}
927+
928+
pub struct Pyo3MethodsInventoryForMyClass {
929+
methods: &'static [pyo3::class::PyMethodDefType],
930+
}
931+
impl pyo3::class::methods::PyMethodsInventory for Pyo3MethodsInventoryForMyClass {
932+
fn new(methods: &'static [pyo3::class::PyMethodDefType]) -> Self {
933+
Self { methods }
934+
}
935+
fn get(&self) -> &'static [pyo3::class::PyMethodDefType] {
936+
self.methods
937+
}
938+
}
939+
impl pyo3::class::methods::PyMethodsImpl for MyClass {
940+
type Methods = Pyo3MethodsInventoryForMyClass;
941+
}
942+
pyo3::inventory::collect!(Pyo3MethodsInventoryForMyClass);
943+
944+
pub struct Pyo3ProtoMethodsInventoryForMyClass {
945+
methods: &'static [pyo3::class::PyMethodDef],
946+
}
947+
impl pyo3::class::methods::PyProtoMethodsInventory for Pyo3ProtoMethodsInventoryForMyClass {
948+
fn new(methods: &'static [pyo3::class::PyMethodDef]) -> Self {
949+
Self { methods }
950+
}
951+
fn get(&self) -> &'static [pyo3::class::PyMethodDef] {
952+
self.methods
953+
}
954+
}
955+
impl pyo3::class::methods::PyProtoMethodsImpl for MyClass {
956+
type ProtoMethods = Pyo3ProtoMethodsInventoryForMyClass;
957+
}
958+
pyo3::inventory::collect!(Pyo3ProtoMethodsInventoryForMyClass);
959+
# let gil = Python::acquire_gil();
960+
# let py = gil.python();
961+
# let cls = py.get_type::<MyClass>();
962+
# pyo3::py_run!(py, cls, "assert cls.__name__ == 'MyClass'")
963+
```
964+
948965

949966
[`GILGuard`]: https://docs.rs/pyo3/latest/pyo3/struct.GILGuard.html
950967
[`PyGCProtocol`]: https://docs.rs/pyo3/latest/pyo3/class/gc/trait.PyGCProtocol.html

pyo3-derive-backend/src/pyclass.rs

+37-13
Original file line numberDiff line numberDiff line change
@@ -208,11 +208,9 @@ fn parse_descriptors(item: &mut syn::Field) -> syn::Result<Vec<FnType>> {
208208
Ok(descs)
209209
}
210210

211-
/// The orphan rule disallows using a generic inventory struct, so we create the whole boilerplate
212-
/// once per class
213-
fn impl_inventory(cls: &syn::Ident) -> TokenStream {
214-
// Try to build a unique type that gives a hint about it's function when
215-
// it comes up in error messages
211+
/// To allow multiple #[pymethods] block, we define inventory types.
212+
fn impl_methods_inventory(cls: &syn::Ident) -> TokenStream {
213+
// Try to build a unique type for better error messages
216214
let name = format!("Pyo3MethodsInventoryFor{}", cls);
217215
let inventory_cls = syn::Ident::new(&name, Span::call_site());
218216

@@ -221,15 +219,11 @@ fn impl_inventory(cls: &syn::Ident) -> TokenStream {
221219
pub struct #inventory_cls {
222220
methods: &'static [pyo3::class::PyMethodDefType],
223221
}
224-
225222
impl pyo3::class::methods::PyMethodsInventory for #inventory_cls {
226223
fn new(methods: &'static [pyo3::class::PyMethodDefType]) -> Self {
227-
Self {
228-
methods
229-
}
224+
Self { methods }
230225
}
231-
232-
fn get_methods(&self) -> &'static [pyo3::class::PyMethodDefType] {
226+
fn get(&self) -> &'static [pyo3::class::PyMethodDefType] {
233227
self.methods
234228
}
235229
}
@@ -242,6 +236,33 @@ fn impl_inventory(cls: &syn::Ident) -> TokenStream {
242236
}
243237
}
244238

239+
/// To avoid some specialization usages in #[pyproto], we define inventory types.
240+
fn impl_proto_inventory(cls: &syn::Ident) -> TokenStream {
241+
let name = format!("Pyo3ProtoMethodsInventoryFor{}", cls);
242+
let inventory_cls = syn::Ident::new(&name, Span::call_site());
243+
244+
quote! {
245+
#[doc(hidden)]
246+
pub struct #inventory_cls {
247+
methods: &'static [pyo3::class::PyMethodDef],
248+
}
249+
impl pyo3::class::methods::PyProtoMethodsInventory for #inventory_cls {
250+
fn new(methods: &'static [pyo3::class::PyMethodDef]) -> Self {
251+
Self { methods }
252+
}
253+
fn get(&self) -> &'static [pyo3::class::PyMethodDef] {
254+
self.methods
255+
}
256+
}
257+
258+
impl pyo3::class::methods::PyProtoMethodsImpl for #cls {
259+
type ProtoMethods = #inventory_cls;
260+
}
261+
262+
pyo3::inventory::collect!(#inventory_cls);
263+
}
264+
}
265+
245266
fn get_class_python_name(cls: &syn::Ident, attr: &PyClassArgs) -> TokenStream {
246267
match &attr.name {
247268
Some(name) => quote! { #name },
@@ -345,7 +366,8 @@ fn impl_class(
345366
quote! {}
346367
};
347368

348-
let inventory_impl = impl_inventory(&cls);
369+
let methods_inventory = impl_methods_inventory(&cls);
370+
let proto_inventory = impl_proto_inventory(&cls);
349371

350372
let base = &attr.base;
351373
let flags = &attr.flags;
@@ -418,7 +440,9 @@ fn impl_class(
418440

419441
#into_pyobject
420442

421-
#inventory_impl
443+
#methods_inventory
444+
445+
#proto_inventory
422446

423447
#extra
424448

pyo3-derive-backend/src/pyproto.rs

+30-31
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub fn build_py_proto(ast: &mut syn::ItemImpl) -> syn::Result<TokenStream> {
3636
));
3737
};
3838

39-
let tokens = impl_proto_impl(&ast.self_ty, &mut ast.items, proto);
39+
let tokens = impl_proto_impl(&ast.self_ty, &mut ast.items, proto)?;
4040

4141
// attach lifetime
4242
let mut seg = path.segments.pop().unwrap().into_value();
@@ -57,53 +57,52 @@ fn impl_proto_impl(
5757
ty: &syn::Type,
5858
impls: &mut Vec<syn::ImplItem>,
5959
proto: &defs::Proto,
60-
) -> TokenStream {
61-
let mut tokens = TokenStream::new();
60+
) -> syn::Result<TokenStream> {
61+
let mut trait_impls = TokenStream::new();
6262
let mut py_methods = Vec::new();
6363

6464
for iimpl in impls.iter_mut() {
6565
if let syn::ImplItem::Method(ref mut met) = iimpl {
6666
if let Some(m) = proto.get_proto(&met.sig.ident) {
67-
impl_method_proto(ty, &mut met.sig, m).to_tokens(&mut tokens);
67+
impl_method_proto(ty, &mut met.sig, m).to_tokens(&mut trait_impls);
6868
}
6969
if let Some(m) = proto.get_method(&met.sig.ident) {
7070
let name = &met.sig.ident;
71-
let proto: syn::Path = syn::parse_str(m.proto).unwrap();
72-
73-
let fn_spec = match FnSpec::parse(&met.sig, &mut met.attrs, false) {
74-
Ok(fn_spec) => fn_spec,
75-
Err(err) => return err.to_compile_error(),
76-
};
77-
let meth = pymethod::impl_proto_wrap(ty, &fn_spec);
71+
let fn_spec = FnSpec::parse(&met.sig, &mut met.attrs, false)?;
72+
let method = pymethod::impl_proto_wrap(ty, &fn_spec);
7873
let coexist = if m.can_coexist {
74+
// We need METH_COEXIST here to prevent __add__ from overriding __radd__
7975
quote!(pyo3::ffi::METH_COEXIST)
8076
} else {
8177
quote!(0)
8278
};
83-
py_methods.push(quote! {
84-
impl #proto for #ty
85-
{
86-
#[inline]
87-
fn #name() -> Option<pyo3::class::methods::PyMethodDef> {
88-
#meth
89-
90-
Some(pyo3::class::PyMethodDef {
91-
ml_name: stringify!(#name),
92-
ml_meth: pyo3::class::PyMethodType::PyCFunctionWithKeywords(__wrap),
93-
// We need METH_COEXIST here to prevent __add__ from overriding __radd__
94-
ml_flags: pyo3::ffi::METH_VARARGS | pyo3::ffi::METH_KEYWORDS | #coexist,
95-
ml_doc: ""
96-
})
97-
}
79+
// TODO(kngwyu): doc
80+
py_methods.push(quote! {{
81+
#method
82+
pyo3::class::PyMethodDef {
83+
ml_name: stringify!(#name),
84+
ml_meth: pyo3::class::PyMethodType::PyCFunctionWithKeywords(__wrap),
85+
ml_flags: pyo3::ffi::METH_VARARGS | pyo3::ffi::METH_KEYWORDS | #coexist,
86+
ml_doc: ""
9887
}
99-
});
88+
}});
10089
}
10190
}
10291
}
10392

104-
quote! {
105-
#tokens
106-
107-
#(#py_methods)*
93+
if py_methods.is_empty() {
94+
return Ok(quote! { #trait_impls });
10895
}
96+
let inventory_submission = quote! {
97+
pyo3::inventory::submit! {
98+
#![crate = pyo3] {
99+
type ProtoInventory = <#ty as pyo3::class::methods::PyProtoMethodsImpl>::ProtoMethods;
100+
<ProtoInventory as pyo3::class::methods::PyProtoMethodsInventory>::new(&[#(#py_methods),*])
101+
}
102+
}
103+
};
104+
Ok(quote! {
105+
#trait_impls
106+
#inventory_submission
107+
})
109108
}

0 commit comments

Comments
 (0)