Skip to content

Commit 853fc7c

Browse files
authored
Merge pull request #332 from PyO3/inventory
Allow 0..n pymethod blocks without specialization
2 parents 9c3ac7e + 328d61c commit 853fc7c

File tree

15 files changed

+165
-131
lines changed

15 files changed

+165
-131
lines changed

Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,21 @@ appveyor = { repository = "fafhrd91/pyo3" }
2020
codecov = { repository = "PyO3/pyo3", branch = "master", service = "github" }
2121

2222
[dependencies]
23-
libc = "0.2.43"
23+
libc = "0.2.48"
2424
spin = "0.5.0"
2525
num-traits = "0.2.6"
2626
pyo3cls = { path = "pyo3cls", version = "=0.6.0-alpha.2" }
2727
mashup = "0.1.9"
2828
num-complex = { version = "0.2.1", optional = true }
29+
inventory = "0.1.3"
2930

3031
[dev-dependencies]
31-
assert_approx_eq = "1.0.0"
32+
assert_approx_eq = "1.1.0"
3233
docmatic = "0.1.2"
3334
indoc = "0.3.1"
3435

3536
[build-dependencies]
36-
regex = "1.0.5"
37+
regex = "1.1.0"
3738
version_check = "0.1.5"
3839

3940
[features]

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ test:
44
cargo test
55
cargo clippy
66
tox
7-
for example in examples/*; do tox -e py --workdir $$example; done
7+
for example in examples/*; do tox -e py -c $$example/tox.ini; done
8+
9+
test_py3:
10+
tox -e py3
11+
for example in examples/*; do tox -e py3 -c $$example/tox.ini; done
812

913
publish:
1014
cargo test

examples/rustapi_module/src/lib.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
#![feature(specialization)]
2-
31
pub mod datetime;
42
pub mod dict_iter;
53
pub mod othermod;

examples/rustapi_module/tests/test_datetime.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,4 +296,4 @@ def test_tz_class_introspection():
296296
tzi = rdt.TzClass()
297297

298298
assert tzi.__class__ == rdt.TzClass
299-
assert repr(tzi).startswith("<rustapi_module.datetime.TzClass object at")
299+
assert repr(tzi).startswith("<TzClass object at")

examples/word-count/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Source adopted from
22
// https://github.com/tildeio/helix-website/blob/master/crates/word_count/src/lib.rs
3-
#![feature(specialization)]
43

54
use pyo3::prelude::*;
65
use pyo3::wrap_pyfunction;

guide/src/class.md

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
To define python custom class, rust struct needs to be annotated with `#[pyclass]` attribute.
66

77
```rust
8-
# #![feature(specialization)]
98
# use pyo3::prelude::*;
109

1110
#[pyclass]
@@ -42,11 +41,8 @@ To declare a constructor, you need to define a class method and annotate it with
4241
attribute. Only the python `__new__` method can be specified, `__init__` is not available.
4342

4443
```rust
45-
# #![feature(specialization)]
46-
#
4744
# use pyo3::prelude::*;
4845
# use pyo3::PyRawObject;
49-
5046
#[pyclass]
5147
struct MyClass {
5248
num: i32,
@@ -86,7 +82,6 @@ By default `PyObject` is used as default base class. To override default base cl
8682
with value of custom class struct. Subclass must call parent's `__new__` method.
8783

8884
```rust
89-
# #![feature(specialization)]
9085
# use pyo3::prelude::*;
9186
# use pyo3::PyRawObject;
9287
#[pyclass]
@@ -136,7 +131,6 @@ Descriptor methods can be defined in
136131
attributes. i.e.
137132

138133
```rust
139-
# #![feature(specialization)]
140134
# use pyo3::prelude::*;
141135
# #[pyclass]
142136
# struct MyClass {
@@ -161,7 +155,6 @@ Descriptor name becomes function name with prefix removed. This is useful in cas
161155
rust's special keywords like `type`.
162156

163157
```rust
164-
# #![feature(specialization)]
165158
# use pyo3::prelude::*;
166159
# #[pyclass]
167160
# struct MyClass {
@@ -190,7 +183,6 @@ Also both `#[getter]` and `#[setter]` attributes accepts one parameter.
190183
If parameter is specified, it is used and property name. i.e.
191184

192185
```rust
193-
# #![feature(specialization)]
194186
# use pyo3::prelude::*;
195187
# #[pyclass]
196188
# struct MyClass {
@@ -218,7 +210,6 @@ In this case property `number` is defined. And it is available from python code
218210
For simple cases you can also define getters and setters in your Rust struct field definition, for example:
219211

220212
```rust
221-
# #![feature(specialization)]
222213
# use pyo3::prelude::*;
223214
#[pyclass]
224215
struct MyClass {
@@ -237,7 +228,6 @@ wrappers for all functions in this block with some variations, like descriptors,
237228
class method static methods, etc.
238229

239230
```rust
240-
# #![feature(specialization)]
241231
# use pyo3::prelude::*;
242232
# #[pyclass]
243233
# struct MyClass {
@@ -265,7 +255,6 @@ The return type must be `PyResult<T>` for some `T` that implements `IntoPyObject
265255
get injected by method wrapper. i.e
266256

267257
```rust
268-
# #![feature(specialization)]
269258
# use pyo3::prelude::*;
270259
# #[pyclass]
271260
# struct MyClass {
@@ -289,7 +278,6 @@ To specify class method for custom class, method needs to be annotated
289278
with`#[classmethod]` attribute.
290279

291280
```rust
292-
# #![feature(specialization)]
293281
# use pyo3::prelude::*;
294282
# #[pyclass]
295283
# struct MyClass {
@@ -321,7 +309,6 @@ with `#[staticmethod]` attribute. The return type must be `PyResult<T>`
321309
for some `T` that implements `IntoPyObject`.
322310

323311
```rust
324-
# #![feature(specialization)]
325312
# use pyo3::prelude::*;
326313
# #[pyclass]
327314
# struct MyClass {
@@ -344,7 +331,6 @@ To specify custom `__call__` method for custom class, call method needs to be an
344331
with `#[call]` attribute. Arguments of the method are specified same as for instance method.
345332

346333
```rust
347-
# #![feature(specialization)]
348334
# use pyo3::prelude::*;
349335
# #[pyclass]
350336
# struct MyClass {
@@ -387,7 +373,6 @@ Each parameter could one of following type:
387373

388374
Example:
389375
```rust
390-
# #![feature(specialization)]
391376
# use pyo3::prelude::*;
392377
#
393378
# #[pyclass]
@@ -556,3 +541,13 @@ impl PyIterProtocol for MyIterator {
556541
}
557542
}
558543
```
544+
545+
## Manually implementing pyclass
546+
547+
TODO: Which traits to implement (basically `PyTypeCreate: PyObjectAlloc + PyTypeInfo + PyMethodsProtocol + Sized`) and what they mean.
548+
549+
## How methods are implemented
550+
551+
Users should be able to define a `#[pyclass]` with or without `#[pymethods]`, while pyo3 needs a trait with a function that returns all methods. Since it's impossible make the code generation in pyclass dependent on whether there is an impl block, we'd need to make to implement the trait on `#[pyclass]` and override the implementation in `#[pymethods]`, which is to my best knowledge only possible with the specialization feature, which is can't be used on stable.
552+
553+
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.

pyo3-derive-backend/src/py_class.rs

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,40 @@ fn parse_descriptors(item: &mut syn::Field) -> Vec<FnType> {
6363
descs
6464
}
6565

66+
/// The orphan rule disallows using a generic inventory struct, so we create the whole boilerplate
67+
/// once per class
68+
fn impl_inventory(cls: &syn::Ident) -> TokenStream {
69+
// Try to build a unique type that gives a hint about it's function when
70+
// it comes up in error messages
71+
let name = cls.to_string() + "GeneratedPyo3Inventory";
72+
let inventory_cls = syn::Ident::new(&name, Span::call_site());
73+
74+
quote! {
75+
#[doc(hidden)]
76+
pub struct #inventory_cls {
77+
methods: &'static [::pyo3::class::PyMethodDefType],
78+
}
79+
80+
impl ::pyo3::class::methods::PyMethodsInventory for #inventory_cls {
81+
fn new(methods: &'static [::pyo3::class::PyMethodDefType]) -> Self {
82+
Self {
83+
methods
84+
}
85+
}
86+
87+
fn get_methods(&self) -> &'static [::pyo3::class::PyMethodDefType] {
88+
self.methods
89+
}
90+
}
91+
92+
impl ::pyo3::class::methods::PyMethodsInventoryDispatch for #cls {
93+
type InventoryType = #inventory_cls;
94+
}
95+
96+
::pyo3::inventory::collect!(#inventory_cls);
97+
}
98+
}
99+
66100
fn impl_class(
67101
cls: &syn::Ident,
68102
base: &syn::TypePath,
@@ -136,6 +170,8 @@ fn impl_class(
136170
quote! {0}
137171
};
138172

173+
let inventory_impl = impl_inventory(&cls);
174+
139175
quote! {
140176
impl ::pyo3::typeob::PyTypeInfo for #cls {
141177
type Type = #cls;
@@ -197,6 +233,8 @@ fn impl_class(
197233
}
198234
}
199235

236+
#inventory_impl
237+
200238
#extra
201239
}
202240
}
@@ -287,12 +325,10 @@ fn impl_descriptors(cls: &syn::Type, descriptors: Vec<(syn::Field, Vec<FnType>)>
287325
quote! {
288326
#(#methods)*
289327

290-
impl ::pyo3::class::methods::PyPropMethodsProtocolImpl for #cls {
291-
fn py_methods() -> &'static [::pyo3::class::PyMethodDefType] {
292-
static METHODS: &'static [::pyo3::class::PyMethodDefType] = &[
293-
#(#py_methods),*
294-
];
295-
METHODS
328+
::pyo3::inventory::submit! {
329+
#![crate = pyo3] {
330+
type ClsInventory = <#cls as ::pyo3::class::methods::PyMethodsInventoryDispatch>::InventoryType;
331+
<ClsInventory as ::pyo3::class::methods::PyMethodsInventory>::new(&[#(#py_methods),*])
296332
}
297333
}
298334
}

pyo3-derive-backend/src/py_impl.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,10 @@ pub fn impl_methods(ty: &syn::Type, impls: &mut Vec<syn::ImplItem>) -> TokenStre
3131
}
3232

3333
quote! {
34-
impl ::pyo3::class::methods::PyMethodsProtocolImpl for #ty {
35-
fn py_methods() -> &'static [::pyo3::class::PyMethodDefType] {
36-
static METHODS: &'static [::pyo3::class::PyMethodDefType] = &[
37-
#(#methods),*
38-
];
39-
METHODS
34+
::pyo3::inventory::submit! {
35+
#![crate = pyo3] {
36+
type TyInventory = <#ty as ::pyo3::class::methods::PyMethodsInventoryDispatch>::InventoryType;
37+
<TyInventory as ::pyo3::class::methods::PyMethodsInventory>::new(&[#(#methods),*])
4038
}
4139
}
4240
}

src/class/methods.rs

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,13 @@ pub struct PySetterDef {
5858
}
5959

6060
unsafe impl Sync for PyMethodDef {}
61+
6162
unsafe impl Sync for ffi::PyMethodDef {}
6263

6364
unsafe impl Sync for PyGetterDef {}
65+
6466
unsafe impl Sync for PySetterDef {}
67+
6568
unsafe impl Sync for ffi::PyGetSetDef {}
6669

6770
impl PyMethodDef {
@@ -110,21 +113,40 @@ impl PySetterDef {
110113
}
111114
}
112115

113-
#[doc(hidden)]
114-
/// The pymethods macro implements this trait so the methods are added to the object
115-
pub trait PyMethodsProtocolImpl {
116-
fn py_methods() -> &'static [PyMethodDefType] {
117-
&[]
118-
}
116+
#[doc(hidden)] // Only to be used through the proc macros, use PyMethodsProtocol in custom code
117+
/// This trait is implemented for all pyclass so to implement the [PyMethodsProtocol]
118+
/// through inventory
119+
pub trait PyMethodsInventoryDispatch {
120+
/// This allows us to get the inventory type when only the pyclass is in scope
121+
type InventoryType: PyMethodsInventory;
119122
}
120123

121-
impl<T> PyMethodsProtocolImpl for T {}
124+
#[doc(hidden)] // Only to be used through the proc macros, use PyMethodsProtocol in custom code
125+
/// Allows arbitrary pymethod blocks to submit their methods, which are eventually collected by pyclass
126+
pub trait PyMethodsInventory: inventory::Collect {
127+
/// Create a new instance
128+
fn new(methods: &'static [PyMethodDefType]) -> Self;
122129

123-
#[doc(hidden)]
124-
pub trait PyPropMethodsProtocolImpl {
125-
fn py_methods() -> &'static [PyMethodDefType] {
126-
&[]
127-
}
130+
/// Returns the methods for a single impl block
131+
fn get_methods(&self) -> &'static [PyMethodDefType];
128132
}
129133

130-
impl<T> PyPropMethodsProtocolImpl for T {}
134+
/// The implementation of tis trait defines which methods a python type has.
135+
///
136+
/// For pyclass derived structs this is implemented by collecting all impl blocks through inventory
137+
pub trait PyMethodsProtocol {
138+
/// Returns all methods that are defined for a class
139+
fn py_methods() -> Vec<&'static PyMethodDefType>;
140+
}
141+
142+
impl<T> PyMethodsProtocol for T
143+
where
144+
T: PyMethodsInventoryDispatch,
145+
{
146+
fn py_methods() -> Vec<&'static PyMethodDefType> {
147+
inventory::iter::<T::InventoryType>
148+
.into_iter()
149+
.flat_map(PyMethodsInventory::get_methods)
150+
.collect()
151+
}
152+
}

src/lib.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@
5555
//! **`src/lib.rs`**
5656
//!
5757
//! ```rust
58-
//! #![feature(specialization)]
59-
//!
6058
//! use pyo3::prelude::*;
6159
//! use pyo3::wrap_pyfunction;
6260
//!
@@ -101,8 +99,6 @@
10199
//! Example program displaying the value of `sys.version`:
102100
//!
103101
//! ```rust
104-
//! #![feature(specialization)]
105-
//!
106102
//! use pyo3::prelude::*;
107103
//! use pyo3::types::PyDict;
108104
//!
@@ -146,12 +142,12 @@ pub use crate::pythonrun::{init_once, prepare_freethreaded_python, GILGuard, GIL
146142
pub use crate::typeob::{PyObjectAlloc, PyRawObject, PyTypeInfo};
147143
pub use crate::types::exceptions;
148144

149-
// We need those types in the macro exports
150-
#[doc(hidden)]
151-
pub use libc;
152145
// We need that reexport for wrap_function
153146
#[doc(hidden)]
154147
pub use mashup;
148+
// We need that reexport for pymethods
149+
#[doc(hidden)]
150+
pub use inventory;
155151

156152
/// Rust FFI declarations for Python
157153
pub mod ffi;
@@ -207,7 +203,7 @@ pub mod proc_macro {
207203
macro_rules! wrap_pyfunction {
208204
($function_name:ident) => {{
209205
// Get the mashup macro and its helpers into scope
210-
use $crate::mashup::*;
206+
use pyo3::mashup::*;
211207

212208
mashup! {
213209
// Make sure this ident matches the one in function_wrapper_ident
@@ -227,7 +223,7 @@ macro_rules! wrap_pyfunction {
227223
#[macro_export]
228224
macro_rules! wrap_pymodule {
229225
($module_name:ident) => {{
230-
use $crate::mashup::*;
226+
use pyo3::mashup::*;
231227

232228
mashup! {
233229
m["method"] = PyInit_ $module_name;

0 commit comments

Comments
 (0)