Skip to content

Commit

Permalink
pymethods: make inventory optional
Browse files Browse the repository at this point in the history
  • Loading branch information
davidhewitt committed Mar 2, 2021
1 parent 67b1a61 commit 1feefb5
Show file tree
Hide file tree
Showing 16 changed files with 363 additions and 168 deletions.
39 changes: 20 additions & 19 deletions .github/workflows/ci.yml
Expand Up @@ -84,32 +84,29 @@ jobs:
name: Prepare LD_LIBRARY_PATH (Ubuntu only)
run: echo LD_LIBRARY_PATH=${pythonLocation}/lib >> $GITHUB_ENV

- name: Build docs
run: cargo doc --no-default-features --features "macros num-bigint num-complex hashbrown" --verbose --target ${{ matrix.platform.rust-target }}
# On PyPy can't execute tests because no embedding API.
- if: matrix.python-version == 'pypy-3.6'
name: Set cargo to build only (PyPy only)
run: echo CARGO_COMMAND="cargo build --lib --tests --no-default-features --verbose" >> $GITHUB_ENV

- name: Build (no features)
run: cargo build --no-default-features --verbose --target ${{ matrix.platform.rust-target }}
- name: Build docs
run: cargo doc

- name: Build (all additive features)
run: cargo build --no-default-features --features "macros num-bigint num-complex hashbrown serde" --verbose --target ${{ matrix.platform.rust-target }}
- name: Test (no features)
run: ${{ env.CARGO_COMMAND }}

# Run tests (except on PyPy, because no embedding API).
- if: matrix.python-version != 'pypy-3.6'
name: Test
run: cargo test --no-default-features --features "macros num-bigint num-complex hashbrown serde" --target ${{ matrix.platform.rust-target }}
- name: Test (all additive features)
run: ${{ env.CARGO_COMMAND }} --features "${{ env.ALL_ADDITIVE_FEATURES }}"

# Run tests again, but in abi3 mode
- if: matrix.python-version != 'pypy-3.6'
name: Test (abi3)
run: cargo test --no-default-features --features "abi3 macros num-bigint num-complex hashbrown serde" --target ${{ matrix.platform.rust-target }}
- name: Test (abi3)
run: ${{ env.CARGO_COMMAND }} --features "abi3 ${{ env.ALL_ADDITIVE_FEATURES }}"

# Run tests again, for abi3-py36 (the minimal Python version)
- if: (matrix.python-version != 'pypy-3.6') && (matrix.python-version != '3.6')
name: Test (abi3-py36)
run: cargo test --no-default-features --features "abi3-py36 macros num-bigint num-complex hashbrown serde" --target ${{ matrix.platform.rust-target }}
- name: Test (abi3-py36)
run: ${{ env.CARGO_COMMAND }} "abi3-py36 ${{ env.ALL_ADDITIVE_FEATURES }}"

- name: Test proc-macro code
run: cargo test --manifest-path=pyo3-macros-backend/Cargo.toml --target ${{ matrix.platform.rust-target }}
run: cargo test --manifest-path=pyo3-macros-backend/Cargo.toml

- name: Install python test dependencies
run: |
Expand All @@ -122,10 +119,14 @@ jobs:
for example_dir in examples/*; do
tox --discover $(which python) -c $example_dir -e py
done
env:
TOX_TESTENV_PASSENV: "CARGO_BUILD_TARGET"
env:
CARGO_BUILD_TARGET: ${{ matrix.platform.rust-target }}
RUST_BACKTRACE: 1
RUSTFLAGS: "-D warnings"
ALL_ADDITIVE_FEATURES: "macros num-bigint num-complex hashbrown serde multiple-pymethods"
CARGO_COMMAND: cargo test --no-default-features --verbose
# TODO: this is a hack to workaround compile_error! warnings about auto-initialize on PyPy
# Once cargo's `resolver = "2"` is stable (~ MSRV Rust 1.52), remove this.
PYO3_CI: 1
Expand Down
5 changes: 4 additions & 1 deletion Cargo.toml
Expand Up @@ -43,7 +43,10 @@ serde_json = "1.0.61"
default = ["macros"]

# 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.
# It tells the linker to keep the python symbols unresolved,
Expand Down
26 changes: 6 additions & 20 deletions guide/src/class.md
Expand Up @@ -332,7 +332,7 @@ impl SubClass {

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 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.

Expand Down Expand Up @@ -711,6 +711,8 @@ See [inventory: how it works](https://github.com/dtolnay/inventory#how-it-works)
Specifically, the following implementation is generated:

```rust
# #[cfg(not(feature = "multiple-pymethods"))]
# { // The implementation differs slightly with the multiple-pymethods feature
use pyo3::prelude::*;

/// Class for demonstration
Expand Down Expand Up @@ -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 {
type ThreadChecker = pyo3::class::impl_::ThreadCheckerStub<MyClass>;

fn for_each_method_def(visitor: impl FnMut(&pyo3::class::PyMethodDefType)) {
use pyo3::class::impl_::*;
let collector = PyClassImplCollector::<MyClass>::new();
pyo3::inventory::iter::<<MyClass as pyo3::class::methods::HasMethodsInventory>::Methods>
.into_iter()
.flat_map(pyo3::class::methods::PyMethodsInventory::get)
collector.py_methods().iter()
.chain(collector.py_class_descriptors())
.chain(collector.object_protocol_methods())
.chain(collector.async_protocol_methods())
.chain(collector.context_protocol_methods())
Expand Down Expand Up @@ -824,6 +809,7 @@ impl pyo3::class::impl_::PyClassImpl for MyClass {
# let py = gil.python();
# let cls = py.get_type::<MyClass>();
# pyo3::py_run!(py, cls, "assert cls.__name__ == 'MyClass'")
# }
```


Expand Down
2 changes: 1 addition & 1 deletion pyo3-macros-backend/src/lib.rs
Expand Up @@ -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 pyclass::{build_py_class, PyClassArgs};
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 utils::get_doc;
43 changes: 30 additions & 13 deletions pyo3-macros-backend/src/pyclass.rs
@@ -1,6 +1,7 @@
// Copyright (c) 2017-present PyO3 Project and Contributors

use crate::method::{FnType, SelfType};
use crate::pyimpl::PyClassMethodsType;
use crate::pymethod::{
impl_py_getter_def, impl_py_setter_def, impl_wrap_getter, impl_wrap_setter, PropertyType,
};
Expand Down Expand Up @@ -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(
&mut class.attrs,
&get_class_python_name(&class.ident, attr),
Expand All @@ -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");
}

impl_class(&class.ident, &attr, doc, descriptors)
impl_class(&class.ident, &attr, doc, descriptors, methods_type)
}

/// Parses `#[pyo3(get, set)]`
Expand Down Expand Up @@ -210,7 +215,7 @@ fn parse_descriptors(item: &mut syn::Field) -> syn::Result<Vec<FnType>> {
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 {
// Try to build a unique type for better error messages
let name = format!("Pyo3MethodsInventoryFor{}", cls.unraw());
Expand All @@ -221,7 +226,7 @@ fn impl_methods_inventory(cls: &syn::Ident) -> TokenStream {
pub struct #inventory_cls {
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 {
Self { methods }
}
Expand All @@ -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;
}

Expand All @@ -247,6 +252,7 @@ fn impl_class(
attr: &PyClassArgs,
doc: syn::LitStr,
descriptors: Vec<(syn::Field, Vec<FnType>)>,
methods_type: PyClassMethodsType,
) -> syn::Result<TokenStream> {
let cls_name = get_class_python_name(cls, attr).to_string();

Expand Down Expand Up @@ -338,7 +344,17 @@ fn impl_class(
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 flags = &attr.flags;
Expand Down Expand Up @@ -429,9 +445,8 @@ fn impl_class(
fn for_each_method_def(visitor: impl FnMut(&pyo3::class::PyMethodDefType)) {
use pyo3::class::impl_::*;
let collector = PyClassImplCollector::<Self>::new();
pyo3::inventory::iter::<<Self as pyo3::class::methods::HasMethodsInventory>::Methods>
.into_iter()
.flat_map(pyo3::class::methods::PyMethodsInventory::get)
#iter_py_methods
.chain(collector.py_class_descriptors())
.chain(collector.object_protocol_methods())
.chain(collector.async_protocol_methods())
.chain(collector.context_protocol_methods())
Expand Down Expand Up @@ -513,10 +528,12 @@ fn impl_descriptors(
.collect::<syn::Result<_>>()?;

Ok(quote! {
pyo3::inventory::submit! {
#![crate = pyo3] {
type Inventory = <#cls as pyo3::class::methods::HasMethodsInventory>::Methods;
<Inventory as pyo3::class::methods::PyMethodsInventory>::new(vec![#(#py_methods),*])
impl pyo3::class::impl_::PyClassDescriptors<#cls>
for pyo3::class::impl_::PyClassImplCollector<#cls>
{
fn py_class_descriptors(self) -> &'static [pyo3::class::methods::PyMethodDefType] {
static METHODS: &[pyo3::class::methods::PyMethodDefType] = &[#(#py_methods),*];
METHODS
}
}
})
Expand Down
53 changes: 47 additions & 6 deletions pyo3-macros-backend/src/pyimpl.rs
Expand Up @@ -6,7 +6,16 @@ use pymethod::GeneratedPyMethod;
use quote::quote;
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_ {
bail_spanned!(path.span() => "#[pymethods] cannot be used on trait impl blocks");
} else if ast.generics != Default::default() {
Expand All @@ -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"
);
} 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 call_impls = Vec::new();
let mut methods = Vec::new();
Expand Down Expand Up @@ -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! {
#(#new_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! {
#![crate = pyo3] {
type Inventory = <#ty as pyo3::class::methods::HasMethodsInventory>::Methods;
<Inventory as pyo3::class::methods::PyMethodsInventory>::new(vec![#(#methods),*])
type Inventory = <#ty as pyo3::class::impl_::HasMethodsInventory>::Methods;
<Inventory as pyo3::class::impl_::PyMethodsInventory>::new(vec![#(#methods),*])
}
}
})
}
}

fn get_cfg_attributes(attrs: &[syn::Attribute]) -> Vec<&syn::Attribute> {
Expand Down
22 changes: 18 additions & 4 deletions pyo3-macros-backend/src/pymethod.rs
Expand Up @@ -666,7 +666,10 @@ pub fn impl_py_method_class_attribute(spec: &FnSpec<'_>, wrapper: &TokenStream)
pyo3::class::PyMethodDefType::ClassAttribute({
#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)
)
})
}
}
Expand All @@ -677,7 +680,10 @@ pub fn impl_py_const_class_attribute(spec: &ConstSpec, wrapper: &TokenStream) ->
pyo3::class::PyMethodDefType::ClassAttribute({
#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)
)
})
}
}
Expand All @@ -703,7 +709,11 @@ pub(crate) fn impl_py_setter_def(
pyo3::class::PyMethodDefType::Setter({
#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
)
})
}
}
Expand All @@ -717,7 +727,11 @@ pub(crate) fn impl_py_getter_def(
pyo3::class::PyMethodDefType::Getter({
#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
)
})
}
}
Expand Down

0 comments on commit 1feefb5

Please sign in to comment.