Merge branch 'PyO3:main' into num-dependency

This commit is contained in:
mejrs 2021-06-05 16:11:45 +02:00
commit d344c0a909
30 changed files with 838 additions and 301 deletions

107
.github/workflows/bench.yml vendored Normal file
View File

@ -0,0 +1,107 @@
on:
push:
branches:
- main
pull_request:
name: Benchmark
jobs:
benchmark:
name: Cargo benchmark
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: cargo-${{ runner.os }}-bench-${{ hashFiles('**/Cargo.toml') }}
continue-on-error: true
- name: Run benchmarks
run: |
for bench in call dict gil list pyclass pyobject set tuple; do
cargo bench --features hashbrown --bench "bench_$bench" -- --output-format bencher | tee -a output.txt
done
# Download previous benchmark result from cache (if exists)
- name: Download previous benchmark data
uses: actions/cache@v1
with:
path: ./cache
key: ${{ runner.os }}-benchmark
# Run `github-action-benchmark` action
- name: Store benchmark result
uses: rhysd/github-action-benchmark@v1
with:
name: pyo3-bench
# What benchmark tool the output.txt came from
tool: 'cargo'
# Where the output from the benchmark tool is stored
output-file-path: output.txt
# # Where the previous data file is stored
# external-data-json-path: ./cache/benchmark-data.json
# Workflow will fail when an alert happens
fail-on-alert: true
# GitHub API token to make a commit comment
github-token: ${{ secrets.GITHUB_TOKEN }}
# Enable alert commit comment
comment-on-alert: true
alert-comment-cc-users: '@PyO3/pyo3'
auto-push: ${{ github.event_name != 'pull_request' }}
pytest-benchmark:
name: pytest benchmark
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: cargo-${{ runner.os }}-pytest-bench-${{ hashFiles('**/Cargo.toml') }}
continue-on-error: true
- name: Download previous benchmark data
uses: actions/cache@v1
with:
path: ./cache
key: ${{ runner.os }}-pytest-benchmark
- name: Run benchmarks
run: |
cd examples/pyo3-benchmarks
python -m pip install -r requirements-dev.txt
python setup.py develop
pytest --benchmark-json ../../output.json
- name: Store benchmark result
uses: rhysd/github-action-benchmark@v1
with:
name: pytest-bench
tool: 'pytest'
output-file-path: output.json
fail-on-alert: true
github-token: ${{ secrets.GITHUB_TOKEN }}
comment-on-alert: true
alert-comment-cc-users: '@PyO3/pyo3'
auto-push: ${{ github.event_name != 'pull_request' }}

View File

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Update `num-complex` optional dependency to 0.4. [#1482](https://github.com/PyO3/pyo3/pull/1482)
- Extend `hashbrown` optional dependency supported versions to include 0.11. [#1496](https://github.com/PyO3/pyo3/pull/1496)
- Support PyPy 3.7. [#1538](https://github.com/PyO3/pyo3/pull/1538)
- Try harder to filter sysconfigdata candidates on arch.config
### Added
- Add conversions for `[T; N]` for all `N` on Rust 1.51 and up. [#1128](https://github.com/PyO3/pyo3/pull/1128)
@ -24,6 +25,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Add `#[pyo3(name = "...")]` syntax for setting Python names. [#1567](https://github.com/PyO3/pyo3/pull/1567)
- Add FFI definition `PyDateTime_TimeZone_UTC`. [#1572](https://github.com/PyO3/pyo3/pull/1572)
- Add support for `#[pyclass(extends=Exception)]`. [#1591](https://github.com/PyO3/pyo3/pull/1591)
- Add support for extracting `PathBuf` from `pathlib.Path`. [#1654](https://github.com/PyO3/pyo3/pull/1654)
### Changed
- Allow only one `#[pymethods]` block per `#[pyclass]` by default, to simplify the proc macro implementations. Add `multiple-pymethods` feature to opt-in to the more complex full behavior. [#1457](https://github.com/PyO3/pyo3/pull/1457)
@ -46,6 +48,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Reduce LLVM line counts to improve compilation times. [#1604](https://github.com/PyO3/pyo3/pull/1604)
- Deprecate string-literal second argument to `#[pyfn(m, "name")]`. [#1610](https://github.com/PyO3/pyo3/pull/1610)
- No longer call `PyEval_InitThreads()` in `#[pymodule]` init code. [#1630](https://github.com/PyO3/pyo3/pull/1630)
- Use `METH_FASTCALL` argument passing convention, when possible, to improve `#[pyfunction]` performance. [#1619](https://github.com/PyO3/pyo3/pull/1619)
### Removed
- Remove deprecated exception names `BaseException` etc. [#1426](https://github.com/PyO3/pyo3/pull/1426)
@ -61,6 +64,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `PyModuleDef_INIT` [#1630](https://github.com/PyO3/pyo3/pull/1630)
- Remove `__doc__` from module's `__all__`. [#1509](https://github.com/PyO3/pyo3/pull/1509)
- Remove `PYO3_CROSS_INCLUDE_DIR` environment variable and the associated C header parsing functionality. [#1521](https://github.com/PyO3/pyo3/pull/1521)
- Remove `raw_pycfunction!` macro. [#1619](https://github.com/PyO3/pyo3/pull/1619)
### Fixed
- Remove FFI definition `PyCFunction_ClearFreeList` for Python 3.9 and later. [#1425](https://github.com/PyO3/pyo3/pull/1425)

View File

@ -17,6 +17,13 @@ fn iter_tuple(b: &mut Bencher) {
});
}
fn tuple_new(b: &mut Bencher) {
let gil = Python::acquire_gil();
let py = gil.python();
const LEN: usize = 50_000;
b.iter(|| PyTuple::new(py, 0..LEN));
}
fn tuple_get_item(b: &mut Bencher) {
let gil = Python::acquire_gil();
let py = gil.python();
@ -32,6 +39,7 @@ fn tuple_get_item(b: &mut Bencher) {
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("iter_tuple", iter_tuple);
c.bench_function("tuple_new", tuple_new);
c.bench_function("tuple_get_item", tuple_get_item);
}

View File

@ -8,6 +8,7 @@ pub mod dict_iter;
pub mod misc;
pub mod objstore;
pub mod othermod;
pub mod path;
pub mod pyclass_iter;
pub mod subclassing;
@ -17,6 +18,7 @@ use dict_iter::*;
use misc::*;
use objstore::*;
use othermod::*;
use path::*;
use pyclass_iter::*;
use subclassing::*;
@ -28,6 +30,7 @@ fn pyo3_pytests(py: Python, m: &PyModule) -> PyResult<()> {
m.add_wrapped(wrap_pymodule!(misc))?;
m.add_wrapped(wrap_pymodule!(objstore))?;
m.add_wrapped(wrap_pymodule!(othermod))?;
m.add_wrapped(wrap_pymodule!(path))?;
m.add_wrapped(wrap_pymodule!(pyclass_iter))?;
m.add_wrapped(wrap_pymodule!(subclassing))?;
@ -42,6 +45,7 @@ fn pyo3_pytests(py: Python, m: &PyModule) -> PyResult<()> {
sys_modules.set_item("pyo3_pytests.misc", m.getattr("misc")?)?;
sys_modules.set_item("pyo3_pytests.objstore", m.getattr("objstore")?)?;
sys_modules.set_item("pyo3_pytests.othermod", m.getattr("othermod")?)?;
sys_modules.set_item("pyo3_pytests.path", m.getattr("path")?)?;
sys_modules.set_item("pyo3_pytests.pyclass_iter", m.getattr("pyclass_iter")?)?;
sys_modules.set_item("pyo3_pytests.subclassing", m.getattr("subclassing")?)?;

View File

@ -0,0 +1,21 @@
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;
use std::path::{Path, PathBuf};
#[pyfunction]
fn make_path() -> PathBuf {
Path::new("/root").to_owned()
}
#[pyfunction]
fn take_pathbuf(path: PathBuf) -> PathBuf {
path
}
#[pymodule]
fn path(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(make_path, m)?)?;
m.add_function(wrap_pyfunction!(take_pathbuf, m)?)?;
Ok(())
}

View File

@ -0,0 +1,18 @@
import pathlib
import pyo3_pytests.path as rpath
def test_make_path():
p = rpath.make_path()
assert p == "/root"
def test_take_pathbuf():
p = "/root"
assert rpath.take_pathbuf(p) == p
def test_take_pathlib():
p = pathlib.Path("/root")
assert rpath.take_pathbuf(p) == str(p)

View File

@ -349,7 +349,7 @@ struct MyClass {
}
```
The above would make the `num` property available for reading and writing from Python code as `self.num`.
The above would make the `num` field available for reading and writing as a `self.num` Python property. To expose the property with a different name to the field, specify this alongside the rest of the options, e.g. `#[pyo3(get, set, name = "custom_name")]`.
Properties can be readonly or writeonly by using just `#[pyo3(get)]` or `#[pyo3(set)]` respectively.

View File

@ -35,7 +35,7 @@ The Rust book suggests to [put integration tests inside a `tests/` directory](ht
For a PyO3 `extension-module` project where the `crate-type` is set to `"cdylib"` in your `Cargo.toml`,
the compiler won't be able to find your crate and will display errors such as `E0432` or `E0463`:
```
```text
error[E0432]: unresolved import `my_crate`
--> tests/test_my_crate.rs:1:5
|
@ -45,7 +45,7 @@ error[E0432]: unresolved import `my_crate`
The best solution is to make your crate types include both `rlib` and `cdylib`:
```
```toml
# Cargo.toml
[lib]
crate-type = ["cdylib", "rlib"]
@ -56,3 +56,87 @@ crate-type = ["cdylib", "rlib"]
This is because Ctrl-C raises a SIGINT signal, which is handled by the calling Python process by simply setting a flag to action upon later. This flag isn't checked while Rust code called from Python is executing, only once control returns to the Python interpreter.
You can give the Python interpreter a chance to process the signal properly by calling `Python::check_signals`. It's good practice to call this function regularly if you have a long-running Rust function so that your users can cancel it.
## `#[pyo3(get)]` clones my field!
You may have a nested struct similar to this:
```rust
# use pyo3::prelude::*;
#[pyclass]
#[derive(Clone)]
struct Inner { /* fields omitted */ }
#[pyclass]
struct Outer {
#[pyo3(get)]
inner: Inner,
}
#[pymethods]
impl Outer {
#[new]
fn __new__() -> Self {
Self { inner: Inner {} }
}
}
```
When Python code accesses `Outer`'s field, PyO3 will return a new object on every access (note that their addresses are different):
```python
outer = Outer()
a = outer.inner
b = outer.inner
assert a is b, f"a: {a}\nb: {b}"
```
```text
AssertionError: a: <builtins.Inner object at 0x00000238FFB9C7B0>
b: <builtins.Inner object at 0x00000238FFB9C830>
```
This can be especially confusing if the field is mutable, as getting the field and then mutating it won't persist - you'll just get a fresh clone of the original on the next access. Unfortunately Python and Rust don't agree about ownership - if PyO3 gave out references to (possibly) temporary Rust objects to Python code, Python code could then keep that reference alive indefinitely. Therefore returning Rust objects requires cloning.
If you don't want that cloning to happen, a workaround is to allocate the field on the Python heap and store a reference to that, by using [`Py<...>`]({{#PYO3_DOCS_URL}}/pyo3/struct.Py.html):
```rust
# use pyo3::prelude::*;
#[pyclass]
#[derive(Clone)]
struct Inner { /* fields omitted */ }
#[pyclass]
struct Outer {
#[pyo3(get)]
inner: Py<Inner>,
}
#[pymethods]
impl Outer {
#[new]
fn __new__(py: Python) -> PyResult<Self> {
Ok(Self {
inner: Py::new(py, Inner {})?,
})
}
}
```
This time `a` and `b` *are* the same object:
```python
outer = Outer()
a = outer.inner
b = outer.inner
assert a is b, f"a: {a}\nb: {b}"
print(f"a: {a}\nb: {b}")
```
```text
a: <builtins.Inner object at 0x0000020044FCC670>
b: <builtins.Inner object at 0x0000020044FCC670>
```
The downside to this approach is that any Rust code working on the `Outer` struct now has to acquire the GIL to do anything with its field.

View File

@ -279,12 +279,11 @@ in the function body.
## Accessing the FFI functions
In order to make Rust functions callable from Python, PyO3 generates a
`extern "C" Fn(slf: *mut PyObject, args: *mut PyObject, kwargs: *mut PyObject) -> *mut Pyobject`
function and embeds the call to the Rust function inside this FFI-wrapper function. This
wrapper handles extraction of the regular arguments and the keyword arguments from the input
`PyObjects`. Since this function is not user-defined but required to build a `PyCFunction`, PyO3
offers the `raw_pycfunction!()` macro to get the identifier of this generated wrapper.
In order to make Rust functions callable from Python, PyO3 generates an `extern "C"`
function whose exact signature depends on the Rust signature. (PyO3 chooses the optimal
Python argument passing convention.) It then embeds the call to the Rust function inside this
FFI-wrapper function. This wrapper handles extraction of the regular arguments and the keyword
arguments from the input `PyObject`s.
The `wrap_pyfunction` macro can be used to directly get a `PyCFunction` given a
`#[pyfunction]` and a `PyModule`: `wrap_pyfunction!(rust_fun, module)`.

View File

@ -484,6 +484,24 @@ fn search_lib_dir(path: impl AsRef<Path>, cross: &CrossCompileConfig) -> Vec<Pat
};
sysconfig_paths.extend(sysc);
}
// If we got more than one file, only take those that contain the arch name.
// For ubuntu 20.04 with host architecture x86_64 and a foreign architecture of armhf
// this reduces the number of candidates to 1:
//
// $ find /usr/lib/python3.8/ -name '_sysconfigdata*.py' -not -lname '*'
// /usr/lib/python3.8/_sysconfigdata__x86_64-linux-gnu.py
// /usr/lib/python3.8/_sysconfigdata__arm-linux-gnueabihf.py
if sysconfig_paths.len() > 1 {
let temp = sysconfig_paths
.iter()
.filter(|p| p.to_string_lossy().contains(&cross.arch))
.cloned()
.collect::<Vec<PathBuf>>();
if !temp.is_empty() {
sysconfig_paths = temp;
}
}
sysconfig_paths
}

View File

@ -12,9 +12,11 @@ pub mod kw {
syn::custom_keyword!(annotation);
syn::custom_keyword!(attribute);
syn::custom_keyword!(from_py_with);
syn::custom_keyword!(get);
syn::custom_keyword!(item);
syn::custom_keyword!(pass_module);
syn::custom_keyword!(name);
syn::custom_keyword!(set);
syn::custom_keyword!(signature);
syn::custom_keyword!(transparent);
}
@ -43,9 +45,7 @@ impl Parse for NameAttribute {
}
}
pub fn get_pyo3_attributes<T: Parse>(
attr: &syn::Attribute,
) -> Result<Option<Punctuated<T, Comma>>> {
pub fn get_pyo3_options<T: Parse>(attr: &syn::Attribute) -> Result<Option<Punctuated<T, Comma>>> {
if is_attribute_ident(attr, "pyo3") {
attr.parse_args_with(Punctuated::parse_terminated).map(Some)
} else {
@ -83,6 +83,19 @@ pub fn take_attributes(
Ok(())
}
pub fn take_pyo3_options<T: Parse>(attrs: &mut Vec<syn::Attribute>) -> Result<Vec<T>> {
let mut out = Vec::new();
take_attributes(attrs, |attr| {
if let Some(options) = get_pyo3_options(attr)? {
out.extend(options.into_iter());
Ok(true)
} else {
Ok(false)
}
})?;
Ok(out)
}
pub fn get_deprecated_name_attribute(
attr: &syn::Attribute,
deprecations: &mut Deprecations,

View File

@ -1,4 +1,4 @@
use crate::attributes::{self, get_pyo3_attributes, FromPyWithAttribute};
use crate::attributes::{self, get_pyo3_options, FromPyWithAttribute};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{
@ -290,7 +290,7 @@ impl ContainerOptions {
annotation: None,
};
for attr in attrs {
if let Some(pyo3_attrs) = get_pyo3_attributes(attr)? {
if let Some(pyo3_attrs) = get_pyo3_options(attr)? {
for pyo3_attr in pyo3_attrs {
match pyo3_attr {
ContainerPyO3Attribute::Transparent(kw) => {
@ -388,7 +388,7 @@ impl FieldPyO3Attributes {
let mut from_py_with = None;
for attr in attrs {
if let Some(pyo3_attrs) = get_pyo3_attributes(attr)? {
if let Some(pyo3_attrs) = get_pyo3_options(attr)? {
for pyo3_attr in pyo3_attrs {
match pyo3_attr {
FieldPyO3Attribute::Getter(field_getter) => {

View File

@ -1,7 +1,7 @@
use crate::{
attributes::{
self, get_deprecated_name_attribute, get_pyo3_attributes, is_attribute_ident,
take_attributes, NameAttribute,
self, get_deprecated_name_attribute, get_pyo3_options, is_attribute_ident, take_attributes,
NameAttribute,
},
deprecations::Deprecations,
};
@ -69,7 +69,7 @@ impl ConstAttributes {
);
attributes.is_class_attr = true;
Ok(true)
} else if let Some(pyo3_attributes) = get_pyo3_attributes(attr)? {
} else if let Some(pyo3_attributes) = get_pyo3_options(attr)? {
for pyo3_attr in pyo3_attributes {
match pyo3_attr {
PyO3ConstAttribute::Name(name) => attributes.set_name(name)?,

View File

@ -158,6 +158,28 @@ pub fn parse_method_receiver(arg: &syn::FnArg) -> syn::Result<SelfType> {
}
impl<'a> FnSpec<'a> {
/// Determine if the function gets passed a *args tuple or **kwargs dict.
pub fn accept_args_kwargs(&self) -> (bool, bool) {
let (mut accept_args, mut accept_kwargs) = (false, false);
for s in &self.attrs {
match s {
Argument::VarArgs(_) => accept_args = true,
Argument::KeywordArgs(_) => accept_kwargs = true,
_ => continue,
}
}
(accept_args, accept_kwargs)
}
/// Return true if the function can use METH_FASTCALL.
///
/// This is true on Py3.7+, except with the stable ABI (abi3).
pub fn can_use_fastcall(&self) -> bool {
cfg!(all(Py_3_7, not(Py_LIMITED_API)))
}
/// Parser function signature and function attributes
pub fn parse(
sig: &'a mut syn::Signature,
@ -220,9 +242,8 @@ impl<'a> FnSpec<'a> {
})
}
pub fn null_terminated_python_name(&self) -> TokenStream {
let name = format!("{}\0", self.python_name);
quote!({#name})
pub fn null_terminated_python_name(&self) -> syn::LitStr {
syn::LitStr::new(&format!("{}\0", self.python_name), self.python_name.span())
}
fn parse_text_signature(

View File

@ -114,7 +114,7 @@ fn get_pyfn_attr(attrs: &mut Vec<syn::Attribute>) -> syn::Result<Option<PyFnArgs
})?;
if let Some(pyfn_args) = &mut pyfn_args {
pyfn_args.options.take_pyo3_attributes(attrs)?;
pyfn_args.options.take_pyo3_options(attrs)?;
}
Ok(pyfn_args)

View File

@ -1,6 +1,6 @@
// Copyright (c) 2017-present PyO3 Project and Contributors
use crate::method::{FnType, SelfType};
use crate::attributes::{self, take_pyo3_options, NameAttribute};
use crate::pyimpl::PyClassMethodsType;
use crate::pymethod::{impl_py_getter_def, impl_py_setter_def, PropertyType};
use crate::utils;
@ -9,7 +9,7 @@ use quote::quote;
use syn::ext::IdentExt;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{parse_quote, spanned::Spanned, Expr, Token};
use syn::{parse_quote, spanned::Spanned, Expr, Result, Token};
/// The parsed arguments of the pyclass macro
pub struct PyClassArgs {
@ -26,7 +26,7 @@ pub struct PyClassArgs {
}
impl Parse for PyClassArgs {
fn parse(input: ParseStream) -> syn::parse::Result<Self> {
fn parse(input: ParseStream) -> Result<Self> {
let mut slf = PyClassArgs::default();
let vars = Punctuated::<Expr, Token![,]>::parse_terminated(input)?;
@ -57,7 +57,7 @@ impl Default for PyClassArgs {
impl PyClassArgs {
/// Adda single expression from the comma separated list in the attribute, which is
/// either a single word or an assignment expression
fn add_expr(&mut self, expr: &Expr) -> syn::parse::Result<()> {
fn add_expr(&mut self, expr: &Expr) -> Result<()> {
match expr {
syn::Expr::Path(exp) if exp.path.segments.len() == 1 => self.add_path(exp),
syn::Expr::Assign(assign) => self.add_assign(assign),
@ -172,63 +172,102 @@ pub fn build_py_class(
&get_class_python_name(&class.ident, attr),
)?;
let doc = utils::get_doc(&class.attrs, text_signature, true)?;
let mut descriptors = Vec::new();
ensure_spanned!(
class.generics.params.is_empty(),
class.generics.span() => "#[pyclass] cannot have generic parameters"
);
match &mut class.fields {
syn::Fields::Named(fields) => {
for field in fields.named.iter_mut() {
let field_descs = parse_descriptors(field)?;
if !field_descs.is_empty() {
descriptors.push((field.clone(), field_descs));
}
}
let field_options = match &mut class.fields {
syn::Fields::Named(fields) => fields
.named
.iter_mut()
.map(|field| {
FieldPyO3Options::take_pyo3_options(&mut field.attrs)
.map(move |options| (&*field, options))
})
.collect::<Result<_>>()?,
syn::Fields::Unnamed(fields) => fields
.unnamed
.iter_mut()
.map(|field| {
FieldPyO3Options::take_pyo3_options(&mut field.attrs)
.map(move |options| (&*field, options))
})
.collect::<Result<_>>()?,
syn::Fields::Unit => {
// No fields for unit struct
Vec::new()
}
syn::Fields::Unnamed(fields) => {
for field in fields.unnamed.iter_mut() {
let field_descs = parse_descriptors(field)?;
if !field_descs.is_empty() {
descriptors.push((field.clone(), field_descs));
}
}
}
syn::Fields::Unit => { /* No fields for unit struct */ }
}
};
impl_class(&class.ident, &attr, doc, descriptors, methods_type)
impl_class(&class.ident, &attr, doc, field_options, methods_type)
}
/// Parses `#[pyo3(get, set)]`
fn parse_descriptors(item: &mut syn::Field) -> syn::Result<Vec<FnType>> {
let mut descs = Vec::new();
let mut new_attrs = Vec::new();
for attr in item.attrs.drain(..) {
if let Ok(syn::Meta::List(list)) = attr.parse_meta() {
if list.path.is_ident("pyo3") {
for meta in list.nested.iter() {
if let syn::NestedMeta::Meta(metaitem) = meta {
if metaitem.path().is_ident("get") {
descs.push(FnType::Getter(SelfType::Receiver { mutable: false }));
} else if metaitem.path().is_ident("set") {
descs.push(FnType::Setter(SelfType::Receiver { mutable: true }));
} else {
bail_spanned!(metaitem.span() => "only get and set are supported");
}
}
}
} else {
new_attrs.push(attr)
}
/// `#[pyo3()]` options for pyclass fields
struct FieldPyO3Options {
get: bool,
set: bool,
name: Option<NameAttribute>,
}
enum FieldPyO3Option {
Get(attributes::kw::get),
Set(attributes::kw::set),
Name(NameAttribute),
}
impl Parse for FieldPyO3Option {
fn parse(input: ParseStream) -> Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(attributes::kw::get) {
input.parse().map(FieldPyO3Option::Get)
} else if lookahead.peek(attributes::kw::set) {
input.parse().map(FieldPyO3Option::Set)
} else if lookahead.peek(attributes::kw::name) {
input.parse().map(FieldPyO3Option::Name)
} else {
new_attrs.push(attr);
Err(lookahead.error())
}
}
item.attrs = new_attrs;
Ok(descs)
}
impl FieldPyO3Options {
fn take_pyo3_options(attrs: &mut Vec<syn::Attribute>) -> Result<Self> {
let mut options = FieldPyO3Options {
get: false,
set: false,
name: None,
};
for option in take_pyo3_options(attrs)? {
match option {
FieldPyO3Option::Get(kw) => {
ensure_spanned!(
!options.get,
kw.span() => "`get` may only be specified once"
);
options.get = true;
}
FieldPyO3Option::Set(kw) => {
ensure_spanned!(
!options.set,
kw.span() => "`set` may only be specified once"
);
options.set = true;
}
FieldPyO3Option::Name(name) => {
ensure_spanned!(
options.name.is_none(),
name.0.span() => "`name` may only be specified once"
);
options.name = Some(name);
}
}
}
Ok(options)
}
}
/// To allow multiple #[pymethods] block, we define inventory types.
@ -267,12 +306,12 @@ fn impl_class(
cls: &syn::Ident,
attr: &PyClassArgs,
doc: syn::LitStr,
descriptors: Vec<(syn::Field, Vec<FnType>)>,
field_options: Vec<(&syn::Field, FieldPyO3Options)>,
methods_type: PyClassMethodsType,
) -> syn::Result<TokenStream> {
let cls_name = get_class_python_name(cls, attr).to_string();
let extra = {
let alloc = {
if let Some(freelist) = &attr.freelist {
quote! {
impl pyo3::freelist::PyClassWithFreeList for #cls {
@ -296,17 +335,7 @@ fn impl_class(
}
};
let extra = if !descriptors.is_empty() {
let path = syn::Path::from(syn::PathSegment::from(cls.clone()));
let ty = syn::Type::from(syn::TypePath { path, qself: None });
let desc_impls = impl_descriptors(&ty, descriptors)?;
quote! {
#desc_impls
#extra
}
} else {
extra
};
let descriptors = impl_descriptors(cls, field_options)?;
// insert space for weak ref
let weakref = if attr.has_weaklist {
@ -481,39 +510,50 @@ fn impl_class(
}
}
#extra
#alloc
#descriptors
#gc_impl
})
}
fn impl_descriptors(
cls: &syn::Type,
descriptors: Vec<(syn::Field, Vec<FnType>)>,
cls: &syn::Ident,
field_options: Vec<(&syn::Field, FieldPyO3Options)>,
) -> syn::Result<TokenStream> {
let py_methods: Vec<TokenStream> = descriptors
.iter()
.flat_map(|(field, fns)| {
fns.iter()
.map(|desc| {
let doc = utils::get_doc(&field.attrs, None, true)
.unwrap_or_else(|_| syn::LitStr::new("", Span::call_site()));
let property_type = PropertyType::Descriptor(
field.ident.as_ref().ok_or_else(
|| err_spanned!(field.span() => "`#[pyo3(get, set)]` is not supported on tuple struct fields")
)?
);
match desc {
FnType::Getter(self_ty) => {
impl_py_getter_def(cls, property_type, self_ty, &doc, &Default::default())
}
FnType::Setter(self_ty) => {
impl_py_setter_def(cls, property_type, self_ty, &doc, &Default::default())
}
_ => unreachable!(),
}
})
.collect::<Vec<syn::Result<TokenStream>>>()
let ty = syn::parse_quote!(#cls);
let py_methods: Vec<TokenStream> = field_options
.into_iter()
.enumerate()
.flat_map(|(field_index, (field, options))| {
let name_err = if options.name.is_some() && !options.get && !options.set {
Some(Err(err_spanned!(options.name.as_ref().unwrap().0.span() => "`name` is useless without `get` or `set`")))
} else {
None
};
let getter = if options.get {
Some(impl_py_getter_def(&ty, PropertyType::Descriptor {
field_index,
field,
python_name: options.name.as_ref()
}))
} else {
None
};
let setter = if options.set {
Some(impl_py_setter_def(&ty, PropertyType::Descriptor {
field_index,
field,
python_name: options.name.as_ref()
}))
} else {
None
};
name_err.into_iter().chain(getter).chain(setter)
})
.collect::<syn::Result<_>>()?;

View File

@ -2,7 +2,7 @@
use crate::{
attributes::{
self, get_deprecated_name_attribute, get_pyo3_attributes, take_attributes,
self, get_deprecated_name_attribute, get_pyo3_options, take_attributes,
FromPyWithAttribute, NameAttribute,
},
deprecations::Deprecations,
@ -62,7 +62,7 @@ impl PyFunctionArgPyO3Attributes {
pub fn from_attrs(attrs: &mut Vec<syn::Attribute>) -> syn::Result<Self> {
let mut attributes = PyFunctionArgPyO3Attributes { from_py_with: None };
take_attributes(attrs, |attr| {
if let Some(pyo3_attrs) = get_pyo3_attributes(attr)? {
if let Some(pyo3_attrs) = get_pyo3_options(attr)? {
for attr in pyo3_attrs {
match attr {
PyFunctionArgPyO3Attribute::FromPyWith(from_py_with) => {
@ -270,13 +270,13 @@ impl Parse for PyFunctionOption {
impl PyFunctionOptions {
pub fn from_attrs(attrs: &mut Vec<syn::Attribute>) -> syn::Result<Self> {
let mut options = PyFunctionOptions::default();
options.take_pyo3_attributes(attrs)?;
options.take_pyo3_options(attrs)?;
Ok(options)
}
pub fn take_pyo3_attributes(&mut self, attrs: &mut Vec<syn::Attribute>) -> syn::Result<()> {
pub fn take_pyo3_options(&mut self, attrs: &mut Vec<syn::Attribute>) -> syn::Result<()> {
take_attributes(attrs, |attr| {
if let Some(pyo3_attributes) = get_pyo3_attributes(attr)? {
if let Some(pyo3_attributes) = get_pyo3_options(attr)? {
self.add_attributes(pyo3_attributes)?;
Ok(true)
} else if let Some(name) = get_deprecated_name_attribute(attr, &mut self.deprecations)?
@ -332,7 +332,7 @@ pub fn build_py_function(
ast: &mut syn::ItemFn,
mut options: PyFunctionOptions,
) -> syn::Result<TokenStream> {
options.take_pyo3_attributes(&mut ast.attrs)?;
options.take_pyo3_options(&mut ast.attrs)?;
Ok(impl_wrap_pyfunction(ast, options)?.1)
}
@ -401,25 +401,29 @@ pub fn impl_wrap_pyfunction(
let name = &func.sig.ident;
let wrapper_ident = format_ident!("__pyo3_raw_{}", name);
let wrapper = function_c_wrapper(name, &wrapper_ident, &spec, options.pass_module)?;
let methoddef = if spec.args.is_empty() {
quote!(noargs)
let (methoddef_meth, cfunc_variant) = if spec.args.is_empty() {
(quote!(noargs), quote!(PyCFunction))
} else if spec.can_use_fastcall() {
(
quote!(fastcall_cfunction_with_keywords),
quote!(PyCFunctionFastWithKeywords),
)
} else {
quote!(cfunction_with_keywords)
};
let cfunc = if spec.args.is_empty() {
quote!(PyCFunction)
} else {
quote!(PyCFunctionWithKeywords)
(
quote!(cfunction_with_keywords),
quote!(PyCFunctionWithKeywords),
)
};
let wrapped_pyfunction = quote! {
#wrapper
pub(crate) fn #function_wrapper_ident<'a>(
args: impl Into<pyo3::derive_utils::PyFunctionArguments<'a>>
) -> pyo3::PyResult<&'a pyo3::types::PyCFunction> {
pyo3::types::PyCFunction::internal_new(
pyo3::class::methods::PyMethodDef:: #methoddef (
pyo3::class::methods::PyMethodDef:: #methoddef_meth (
#python_name,
pyo3::class::methods:: #cfunc (#wrapper_ident),
pyo3::class::methods:: #cfunc_variant (#wrapper_ident),
#doc,
),
args.into(),
@ -469,8 +473,36 @@ fn function_c_wrapper(
})
}
})
} else if spec.can_use_fastcall() {
let body = impl_arg_params(spec, None, cb, &py, true)?;
Ok(quote! {
unsafe extern "C" fn #wrapper_ident(
_slf: *mut pyo3::ffi::PyObject,
_args: *const *mut pyo3::ffi::PyObject,
_nargs: pyo3::ffi::Py_ssize_t,
_kwnames: *mut pyo3::ffi::PyObject) -> *mut pyo3::ffi::PyObject
{
pyo3::callback::handle_panic(|#py| {
#slf_module
// _nargs is the number of positional arguments in the _args array,
// the number of KW args is given by the length of _kwnames
let _kwnames: Option<&pyo3::types::PyTuple> = #py.from_borrowed_ptr_or_opt(_kwnames);
// Safety: &PyAny has the same memory layout as `*mut ffi::PyObject`
let _args = _args as *const &pyo3::PyAny;
let _kwargs = if let Some(kwnames) = _kwnames {
std::slice::from_raw_parts(_args.offset(_nargs), kwnames.len())
} else {
&[]
};
let _args = std::slice::from_raw_parts(_args, _nargs as usize);
#body
})
}
})
} else {
let body = impl_arg_params(spec, None, cb, &py)?;
let body = impl_arg_params(spec, None, cb, &py, false)?;
Ok(quote! {
unsafe extern "C" fn #wrapper_ident(
_slf: *mut pyo3::ffi::PyObject,
@ -482,7 +514,6 @@ fn function_c_wrapper(
#slf_module
let _args = #py.from_borrowed_ptr::<pyo3::types::PyTuple>(_args);
let _kwargs: Option<&pyo3::types::PyDict> = #py.from_borrowed_ptr_or_opt(_kwargs);
#body
})
}

View File

@ -1,3 +1,6 @@
use std::borrow::Cow;
use crate::attributes::NameAttribute;
use crate::utils::ensure_not_async_fn;
// Copyright (c) 2017-present PyO3 Project and Contributors
use crate::{attributes::FromPyWithAttribute, konst::ConstSpec};
@ -10,12 +13,6 @@ use proc_macro2::{Span, TokenStream};
use quote::{quote, quote_spanned};
use syn::{ext::IdentExt, spanned::Spanned, Result};
#[derive(Clone, Copy)]
pub enum PropertyType<'a> {
Descriptor(&'a syn::Ident),
Function(&'a FnSpec<'a>),
}
pub enum GeneratedPyMethod {
Method(TokenStream),
New(TokenStream),
@ -45,19 +42,19 @@ pub fn gen_py_method(
FnType::ClassAttribute => {
GeneratedPyMethod::Method(impl_py_method_class_attribute(cls, &spec))
}
FnType::Getter(self_ty) => GeneratedPyMethod::Method(impl_py_getter_def(
FnType::Getter(self_type) => GeneratedPyMethod::Method(impl_py_getter_def(
cls,
PropertyType::Function(&spec),
self_ty,
&spec.doc,
&spec.deprecations,
PropertyType::Function {
self_type,
spec: &spec,
},
)?),
FnType::Setter(self_ty) => GeneratedPyMethod::Method(impl_py_setter_def(
FnType::Setter(self_type) => GeneratedPyMethod::Method(impl_py_setter_def(
cls,
PropertyType::Function(&spec),
self_ty,
&spec.doc,
&spec.deprecations,
PropertyType::Function {
self_type,
spec: &spec,
},
)?),
})
}
@ -96,7 +93,7 @@ pub fn impl_wrap_cfunction_with_keywords(
let body = impl_call(cls, &spec);
let slf = self_ty.receiver(cls);
let py = syn::Ident::new("_py", Span::call_site());
let body = impl_arg_params(&spec, Some(cls), body, &py)?;
let body = impl_arg_params(&spec, Some(cls), body, &py, false)?;
let deprecations = &spec.deprecations;
Ok(quote! {{
unsafe extern "C" fn __wrap(
@ -117,6 +114,42 @@ pub fn impl_wrap_cfunction_with_keywords(
}})
}
/// Generate function wrapper for PyCFunctionFastWithKeywords
pub fn impl_wrap_fastcall_cfunction_with_keywords(
cls: &syn::Type,
spec: &FnSpec<'_>,
self_ty: &SelfType,
) -> Result<TokenStream> {
let body = impl_call(cls, &spec);
let slf = self_ty.receiver(cls);
let py = syn::Ident::new("_py", Span::call_site());
let body = impl_arg_params(&spec, Some(cls), body, &py, true)?;
Ok(quote! {{
unsafe extern "C" fn __wrap(
_slf: *mut pyo3::ffi::PyObject,
_args: *const *mut pyo3::ffi::PyObject,
_nargs: pyo3::ffi::Py_ssize_t,
_kwnames: *mut pyo3::ffi::PyObject) -> *mut pyo3::ffi::PyObject
{
pyo3::callback::handle_panic(|#py| {
#slf
let _kwnames: Option<&pyo3::types::PyTuple> = #py.from_borrowed_ptr_or_opt(_kwnames);
// Safety: &PyAny has the same memory layout as `*mut ffi::PyObject`
let _args = _args as *const &pyo3::PyAny;
let _kwargs = if let Some(kwnames) = _kwnames {
std::slice::from_raw_parts(_args.offset(_nargs), kwnames.len())
} else {
&[]
};
let _args = std::slice::from_raw_parts(_args, _nargs as usize);
#body
})
}
__wrap
}})
}
/// Generate function wrapper PyCFunction
pub fn impl_wrap_noargs(cls: &syn::Type, spec: &FnSpec<'_>, self_ty: &SelfType) -> TokenStream {
let body = impl_call(cls, &spec);
@ -145,7 +178,7 @@ pub fn impl_wrap_new(cls: &syn::Type, spec: &FnSpec<'_>) -> Result<TokenStream>
let names: Vec<syn::Ident> = get_arg_names(&spec);
let cb = quote! { #cls::#name(#(#names),*) };
let py = syn::Ident::new("_py", Span::call_site());
let body = impl_arg_params(spec, Some(cls), cb, &py)?;
let body = impl_arg_params(spec, Some(cls), cb, &py, false)?;
let deprecations = &spec.deprecations;
Ok(quote! {{
#[allow(unused_mut)]
@ -175,7 +208,7 @@ pub fn impl_wrap_class(cls: &syn::Type, spec: &FnSpec<'_>) -> Result<TokenStream
let names: Vec<syn::Ident> = get_arg_names(&spec);
let cb = quote! { pyo3::callback::convert(_py, #cls::#name(&_cls, #(#names),*)) };
let py = syn::Ident::new("_py", Span::call_site());
let body = impl_arg_params(spec, Some(cls), cb, &py)?;
let body = impl_arg_params(spec, Some(cls), cb, &py, false)?;
let deprecations = &spec.deprecations;
Ok(quote! {{
#[allow(unused_mut)]
@ -203,7 +236,7 @@ pub fn impl_wrap_static(cls: &syn::Type, spec: &FnSpec<'_>) -> Result<TokenStrea
let names: Vec<syn::Ident> = get_arg_names(&spec);
let cb = quote! { pyo3::callback::convert(_py, #cls::#name(#(#names),*)) };
let py = syn::Ident::new("_py", Span::call_site());
let body = impl_arg_params(spec, Some(cls), cb, &py)?;
let body = impl_arg_params(spec, Some(cls), cb, &py, false)?;
let deprecations = &spec.deprecations;
Ok(quote! {{
#[allow(unused_mut)]
@ -260,17 +293,30 @@ fn impl_call_getter(cls: &syn::Type, spec: &FnSpec) -> syn::Result<TokenStream>
/// Generate a function wrapper called `__wrap` for a property getter
pub(crate) fn impl_wrap_getter(
cls: &syn::Type,
property_type: PropertyType,
self_ty: &SelfType,
property_type: &PropertyType,
) -> syn::Result<TokenStream> {
let getter_impl = match &property_type {
PropertyType::Descriptor(ident) => {
let getter_impl = match property_type {
PropertyType::Descriptor {
field: syn::Field {
ident: Some(ident), ..
},
..
} => {
// named struct field
quote!(_slf.#ident.clone())
}
PropertyType::Function(spec) => impl_call_getter(cls, spec)?,
PropertyType::Descriptor { field_index, .. } => {
// tuple struct field
let index = syn::Index::from(*field_index);
quote!(_slf.#index.clone())
}
PropertyType::Function { spec, .. } => impl_call_getter(cls, spec)?,
};
let slf = self_ty.receiver(cls);
let slf = match property_type {
PropertyType::Descriptor { .. } => SelfType::Receiver { mutable: false }.receiver(cls),
PropertyType::Function { self_type, .. } => self_type.receiver(cls),
};
Ok(quote! {{
unsafe extern "C" fn __wrap(
_slf: *mut pyo3::ffi::PyObject, _: *mut std::os::raw::c_void) -> *mut pyo3::ffi::PyObject
@ -309,17 +355,30 @@ fn impl_call_setter(cls: &syn::Type, spec: &FnSpec) -> syn::Result<TokenStream>
/// Generate a function wrapper called `__wrap` for a property setter
pub(crate) fn impl_wrap_setter(
cls: &syn::Type,
property_type: PropertyType,
self_ty: &SelfType,
property_type: &PropertyType,
) -> syn::Result<TokenStream> {
let setter_impl = match &property_type {
PropertyType::Descriptor(ident) => {
let setter_impl = match property_type {
PropertyType::Descriptor {
field: syn::Field {
ident: Some(ident), ..
},
..
} => {
// named struct field
quote!({ _slf.#ident = _val; })
}
PropertyType::Function(spec) => impl_call_setter(cls, spec)?,
PropertyType::Descriptor { field_index, .. } => {
// tuple struct field
let index = syn::Index::from(*field_index);
quote!({ _slf.#index = _val; })
}
PropertyType::Function { spec, .. } => impl_call_setter(cls, spec)?,
};
let slf = self_ty.receiver(cls);
let slf = match property_type {
PropertyType::Descriptor { .. } => SelfType::Receiver { mutable: true }.receiver(cls),
PropertyType::Function { self_type, .. } => self_type.receiver(cls),
};
Ok(quote! {{
#[allow(unused_mut)]
unsafe extern "C" fn __wrap(
@ -356,6 +415,7 @@ pub fn impl_arg_params(
self_: Option<&syn::Type>,
body: TokenStream,
py: &syn::Ident,
fastcall: bool,
) -> Result<TokenStream> {
if spec.args.is_empty() {
return Ok(body);
@ -405,16 +465,7 @@ pub fn impl_arg_params(
)?);
}
let (mut accept_args, mut accept_kwargs) = (false, false);
for s in spec.attrs.iter() {
use crate::pyfunction::Argument;
match s {
Argument::VarArgs(_) => accept_args = true,
Argument::KeywordArgs(_) => accept_kwargs = true,
_ => continue,
}
}
let (accept_args, accept_kwargs) = spec.accept_args_kwargs();
let cls_name = if let Some(cls) = self_ {
quote! { Some(<#cls as pyo3::type_object::PyTypeInfo>::NAME) }
@ -423,6 +474,24 @@ pub fn impl_arg_params(
};
let python_name = &spec.python_name;
let (args_to_extract, kwargs_to_extract) = if fastcall {
// _args is a &[&PyAny], _kwnames is a Option<&PyTuple> containing the
// keyword names of the keyword args in _kwargs
(
// need copied() for &&PyAny -> &PyAny
quote! { _args.iter().copied() },
quote! { _kwnames.map(|kwnames| {
kwnames.as_slice().iter().copied().zip(_kwargs.iter().copied())
}) },
)
} else {
// _args is a &PyTuple, _kwargs is an Option<&PyDict>
(
quote! { _args.iter() },
quote! { _kwargs.map(|dict| dict.iter()) },
)
};
// create array of arguments, and then parse
Ok(quote! {
{
@ -439,7 +508,12 @@ pub fn impl_arg_params(
};
let mut #args_array = [None; #num_params];
let (_args, _kwargs) = DESCRIPTION.extract_arguments(_args, _kwargs, &mut #args_array)?;
let (_args, _kwargs) = DESCRIPTION.extract_arguments(
#py,
#args_to_extract,
#kwargs_to_extract,
&mut #args_array
)?;
#(#param_conversion)*
@ -593,32 +667,36 @@ pub fn impl_py_method_def(
let add_flags = flags.map(|flags| quote!(.flags(#flags)));
let python_name = spec.null_terminated_python_name();
let doc = &spec.doc;
if spec.args.is_empty() {
let wrapper = impl_wrap_noargs(cls, spec, self_ty);
Ok(quote! {
pyo3::class::PyMethodDefType::Method({
pyo3::class::PyMethodDef::noargs(
#python_name,
pyo3::class::methods::PyCFunction(#wrapper),
#doc
)
#add_flags
})
})
let (methoddef_meth, cfunc_variant) = if spec.args.is_empty() {
(quote!(noargs), quote!(PyCFunction))
} else if spec.can_use_fastcall() {
(
quote!(fastcall_cfunction_with_keywords),
quote!(PyCFunctionFastWithKeywords),
)
} else {
let wrapper = impl_wrap_cfunction_with_keywords(cls, &spec, self_ty)?;
Ok(quote! {
pyo3::class::PyMethodDefType::Method({
pyo3::class::PyMethodDef::cfunction_with_keywords(
#python_name,
pyo3::class::methods::PyCFunctionWithKeywords(#wrapper),
#doc
)
#add_flags
})
(
quote!(cfunction_with_keywords),
quote!(PyCFunctionWithKeywords),
)
};
let wrapper = if spec.args.is_empty() {
impl_wrap_noargs(cls, spec, self_ty)
} else if spec.can_use_fastcall() {
impl_wrap_fastcall_cfunction_with_keywords(cls, &spec, self_ty)?
} else {
impl_wrap_cfunction_with_keywords(cls, &spec, self_ty)?
};
Ok(quote! {
pyo3::class::PyMethodDefType::Method({
pyo3::class::PyMethodDef:: #methoddef_meth (
#python_name,
pyo3::class::methods:: #cfunc_variant (#wrapper),
#doc
)
#add_flags
})
}
})
}
pub fn impl_py_method_def_new(cls: &syn::Type, spec: &FnSpec) -> Result<TokenStream> {
@ -707,18 +785,11 @@ pub fn impl_py_method_def_call(
pub(crate) fn impl_py_setter_def(
cls: &syn::Type,
property_type: PropertyType,
self_ty: &SelfType,
doc: &syn::LitStr,
deprecations: &Deprecations,
) -> Result<TokenStream> {
let python_name = match property_type {
PropertyType::Descriptor(ident) => {
let formatted_name = format!("{}\0", ident.unraw());
quote!(#formatted_name)
}
PropertyType::Function(spec) => spec.null_terminated_python_name(),
};
let wrapper = impl_wrap_setter(cls, property_type, self_ty)?;
let python_name = property_type.null_terminated_python_name()?;
let deprecations = property_type.deprecations();
let doc = property_type.doc();
let wrapper = impl_wrap_setter(cls, &property_type)?;
Ok(quote! {
pyo3::class::PyMethodDefType::Setter({
#deprecations
@ -734,18 +805,11 @@ pub(crate) fn impl_py_setter_def(
pub(crate) fn impl_py_getter_def(
cls: &syn::Type,
property_type: PropertyType,
self_ty: &SelfType,
doc: &syn::LitStr,
deprecations: &Deprecations,
) -> Result<TokenStream> {
let python_name = match property_type {
PropertyType::Descriptor(ident) => {
let formatted_name = format!("{}\0", ident.unraw());
quote!(#formatted_name)
}
PropertyType::Function(spec) => spec.null_terminated_python_name(),
};
let wrapper = impl_wrap_getter(cls, property_type, self_ty)?;
let python_name = property_type.null_terminated_python_name()?;
let deprecations = property_type.deprecations();
let doc = property_type.doc();
let wrapper = impl_wrap_getter(cls, &property_type)?;
Ok(quote! {
pyo3::class::PyMethodDefType::Getter({
#deprecations
@ -770,3 +834,53 @@ fn split_off_python_arg<'a>(args: &'a [FnArg<'a>]) -> (Option<&FnArg>, &[FnArg])
(None, args)
}
}
pub enum PropertyType<'a> {
Descriptor {
field_index: usize,
field: &'a syn::Field,
python_name: Option<&'a NameAttribute>,
},
Function {
self_type: &'a SelfType,
spec: &'a FnSpec<'a>,
},
}
impl PropertyType<'_> {
fn null_terminated_python_name(&self) -> Result<syn::LitStr> {
match self {
PropertyType::Descriptor {
field, python_name, ..
} => {
let name = match (python_name, &field.ident) {
(Some(name), _) => name.0.to_string(),
(None, Some(field_name)) => format!("{}\0", field_name.unraw()),
(None, None) => {
bail_spanned!(field.span() => "`get` and `set` with tuple struct fields require `name`")
}
};
Ok(syn::LitStr::new(&name, field.span()))
}
PropertyType::Function { spec, .. } => Ok(spec.null_terminated_python_name()),
}
}
fn deprecations(&self) -> Option<&Deprecations> {
match self {
PropertyType::Descriptor { .. } => None,
PropertyType::Function { spec, .. } => Some(&spec.deprecations),
}
}
fn doc(&self) -> Cow<syn::LitStr> {
match self {
PropertyType::Descriptor { field, .. } => {
let doc = utils::get_doc(&field.attrs, None, true)
.unwrap_or_else(|_| syn::LitStr::new("", Span::call_site()));
Cow::Owned(doc)
}
PropertyType::Function { spec, .. } => Cow::Borrowed(&spec.doc),
}
}
}

View File

@ -28,6 +28,8 @@ pub enum PyMethodDefType {
pub enum PyMethodType {
PyCFunction(PyCFunction),
PyCFunctionWithKeywords(PyCFunctionWithKeywords),
#[cfg(all(Py_3_7, not(Py_LIMITED_API)))]
PyCFunctionFastWithKeywords(PyCFunctionFastWithKeywords),
}
// These newtype structs serve no purpose other than wrapping which are function pointers - because
@ -36,6 +38,9 @@ pub enum PyMethodType {
pub struct PyCFunction(pub ffi::PyCFunction);
#[derive(Clone, Copy, Debug)]
pub struct PyCFunctionWithKeywords(pub ffi::PyCFunctionWithKeywords);
#[cfg(all(Py_3_7, 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)]
@ -105,6 +110,21 @@ impl PyMethodDef {
}
}
/// Define a function that can take `*args` and `**kwargs`.
#[cfg(all(Py_3_7, 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
@ -115,6 +135,10 @@ impl PyMethodDef {
let meth = match self.ml_meth {
PyMethodType::PyCFunction(meth) => meth.0,
PyMethodType::PyCFunctionWithKeywords(meth) => unsafe { std::mem::transmute(meth.0) },
#[cfg(all(Py_3_7, not(Py_LIMITED_API)))]
PyMethodType::PyCFunctionFastWithKeywords(meth) => unsafe {
std::mem::transmute(meth.0)
},
};
Ok(ffi::PyMethodDef {

View File

@ -1,4 +1,5 @@
use crate::{FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject};
use crate::types::PyType;
use crate::{FromPyObject, IntoPy, PyAny, PyNativeType, PyObject, PyResult, Python, ToPyObject};
use std::borrow::Cow;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
@ -13,7 +14,21 @@ impl ToPyObject for Path {
impl FromPyObject<'_> for PathBuf {
fn extract(ob: &PyAny) -> PyResult<Self> {
Ok(PathBuf::from(OsString::extract(ob)?))
let os_str = match OsString::extract(ob) {
Ok(s) => s,
Err(err) => {
let py = ob.py();
let pathlib = py.import("pathlib")?;
let pathlib_path: &PyType = pathlib.getattr("Path")?.downcast()?;
if pathlib_path.is_instance(ob)? {
let path_str = ob.call_method0("__str__")?;
OsString::extract(path_str)?
} else {
return Err(err);
}
}
};
Ok(PathBuf::from(os_str))
}
}

View File

@ -39,6 +39,7 @@ impl FunctionDescription {
format!("{}()", self.func_name)
}
}
/// Extracts the `args` and `kwargs` provided into `output`, according to this function
/// definition.
///
@ -52,8 +53,9 @@ impl FunctionDescription {
/// Unexpected, duplicate or invalid arguments will cause this function to return `TypeError`.
pub fn extract_arguments<'p>(
&self,
args: &'p PyTuple,
kwargs: Option<&'p PyDict>,
py: Python<'p>,
mut args: impl ExactSizeIterator<Item = &'p PyAny>,
kwargs: Option<impl Iterator<Item = (&'p PyAny, &'p PyAny)>>,
output: &mut [Option<&'p PyAny>],
) -> PyResult<(Option<&'p PyTuple>, Option<&'p PyDict>)> {
let num_positional_parameters = self.positional_parameter_names.len();
@ -66,33 +68,36 @@ impl FunctionDescription {
);
// Handle positional arguments
let (args_provided, varargs) = {
let args_provided = {
let args_provided = args.len();
if self.accept_varargs {
(
std::cmp::min(num_positional_parameters, args_provided),
Some(args.slice(num_positional_parameters as isize, args_provided as isize)),
)
std::cmp::min(num_positional_parameters, args_provided)
} else if args_provided > num_positional_parameters {
return Err(self.too_many_positional_arguments(args_provided));
} else {
(args_provided, None)
args_provided
}
};
// Copy positional arguments into output
for (out, arg) in output[..args_provided].iter_mut().zip(args) {
for (out, arg) in output[..args_provided].iter_mut().zip(args.by_ref()) {
*out = Some(arg);
}
// Collect varargs into tuple
let varargs = if self.accept_varargs {
Some(PyTuple::new(py, args))
} else {
None
};
// Handle keyword arguments
let varkeywords = match (kwargs, self.accept_varkeywords) {
(Some(kwargs), true) => {
let mut varkeywords = None;
self.extract_keyword_arguments(kwargs, output, |name, value| {
varkeywords
.get_or_insert_with(|| PyDict::new(kwargs.py()))
.get_or_insert_with(|| PyDict::new(py))
.set_item(name, value)
})?;
varkeywords
@ -146,7 +151,7 @@ impl FunctionDescription {
#[inline]
fn extract_keyword_arguments<'p>(
&self,
kwargs: &'p PyDict,
kwargs: impl Iterator<Item = (&'p PyAny, &'p PyAny)>,
output: &mut [Option<&'p PyAny>],
mut unexpected_keyword_handler: impl FnMut(&'p PyAny, &'p PyAny) -> PyResult<()>,
) -> PyResult<()> {

View File

@ -45,7 +45,7 @@ pub type PyCFunctionWithKeywords = unsafe extern "C" fn(
kwds: *mut PyObject,
) -> *mut PyObject;
#[cfg(Py_3_7)]
#[cfg(all(Py_3_7, not(Py_LIMITED_API)))]
pub type _PyCFunctionFastWithKeywords = unsafe extern "C" fn(
slf: *mut PyObject,
args: *const *mut PyObject,

View File

@ -337,35 +337,6 @@ macro_rules! wrap_pyfunction {
};
}
/// Returns the function that is called in the C-FFI.
///
/// Use this together with `#[pyfunction]` and [types::PyCFunction].
/// ```
/// use pyo3::prelude::*;
/// use pyo3::types::PyCFunction;
/// use pyo3::raw_pycfunction;
///
/// #[pyfunction]
/// fn some_fun(arg: i32) -> PyResult<()> {
/// Ok(())
/// }
///
/// #[pymodule]
/// fn module(_py: Python, module: &PyModule) -> PyResult<()> {
/// let ffi_wrapper_fun = raw_pycfunction!(some_fun);
/// let docs = "Some documentation string with null-termination\0";
/// let py_cfunction =
/// PyCFunction::new_with_keywords(ffi_wrapper_fun, "function_name", docs, module.into())?;
/// module.add_function(py_cfunction)
/// }
/// ```
#[macro_export]
macro_rules! raw_pycfunction {
($function_name: ident) => {{
pyo3::paste::expr! { [<__pyo3_raw_ $function_name>] }
}};
}
/// Returns a function that takes a [Python] instance and returns a Python module.
///
/// Use this together with `#[pymodule]` and [types::PyModule::add_wrapped].
@ -538,4 +509,5 @@ pub mod doc_test {
doctest!("guide/src/rust_cpython.md", guide_rust_cpython_md);
doctest!("guide/src/trait_bounds.md", guide_trait_bounds_md);
doctest!("guide/src/types.md", guide_types_md);
doctest!("guide/src/faq.md", faq);
}

View File

@ -14,8 +14,6 @@ pyobject_native_type_core!(PyCFunction, ffi::PyCFunction_Type, #checkfunction=ff
impl PyCFunction {
/// Create a new built-in function with keywords.
///
/// See [raw_pycfunction] for documentation on how to get the `fun` argument.
pub fn new_with_keywords<'a>(
fun: ffi::PyCFunctionWithKeywords,
name: &'static str,

View File

@ -26,6 +26,9 @@ impl PyTuple {
unsafe {
let ptr = ffi::PyTuple_New(len as Py_ssize_t);
for (i, e) in elements_iter.enumerate() {
#[cfg(not(Py_LIMITED_API))]
ffi::PyTuple_SET_ITEM(ptr, i as Py_ssize_t, e.to_object(py).into_ptr());
#[cfg(Py_LIMITED_API)]
ffi::PyTuple_SetItem(ptr, i as Py_ssize_t, e.to_object(py).into_ptr());
}
py.from_owned_ptr(ptr)
@ -40,8 +43,12 @@ impl PyTuple {
/// Gets the length of the tuple.
pub fn len(&self) -> usize {
unsafe {
#[cfg(not(Py_LIMITED_API))]
let size = ffi::PyTuple_GET_SIZE(self.as_ptr());
#[cfg(Py_LIMITED_API)]
let size = ffi::PyTuple_Size(self.as_ptr());
// non-negative Py_ssize_t should always fit into Rust uint
ffi::PyTuple_Size(self.as_ptr()) as usize
size as usize
}
}
@ -72,8 +79,12 @@ impl PyTuple {
pub fn get_item(&self, index: usize) -> &PyAny {
assert!(index < self.len());
unsafe {
self.py()
.from_borrowed_ptr(ffi::PyTuple_GetItem(self.as_ptr(), index as Py_ssize_t))
#[cfg(not(Py_LIMITED_API))]
let item = ffi::PyTuple_GET_ITEM(self.as_ptr(), index as Py_ssize_t);
#[cfg(Py_LIMITED_API)]
let item = ffi::PyTuple_GetItem(self.as_ptr(), index as Py_ssize_t);
self.py().from_borrowed_ptr(item)
}
}
@ -122,6 +133,12 @@ impl<'a> Iterator for PyTupleIterator<'a> {
}
}
impl<'a> ExactSizeIterator for PyTupleIterator<'a> {
fn len(&self) -> usize {
self.length - self.index
}
}
impl<'a> IntoIterator for &'a PyTuple {
type Item = &'a PyAny;
type IntoIter = PyTupleIterator<'a>;

View File

@ -317,7 +317,7 @@ fn test_pymethods_from_py_with() {
}
#[pyclass]
struct TupleClass(i32);
struct TupleClass(#[pyo3(get, set, name = "value")] i32);
#[test]
fn test_tuple_struct_class() {
@ -326,5 +326,18 @@ fn test_tuple_struct_class() {
assert!(typeobj.call((), None).is_err());
py_assert!(py, typeobj, "typeobj.__name__ == 'TupleClass'");
let instance = Py::new(py, TupleClass(5)).unwrap();
py_run!(
py,
instance,
r#"
assert instance.value == 5;
instance.value = 1234;
assert instance.value == 1234;
"#
);
assert_eq!(instance.borrow(py).0, 1234);
});
}

View File

@ -159,7 +159,7 @@ mod inheriting_native_type {
#[pyclass(extends=PySet)]
#[derive(Debug)]
struct SetWithName {
#[pyo3(get(name))]
#[pyo3(get, name = "name")]
_name: &'static str,
}
@ -179,14 +179,14 @@ mod inheriting_native_type {
py_run!(
py,
set_sub,
r#"set_sub.add(10); assert list(set_sub) == [10]; assert set_sub._name == "Hello :)""#
r#"set_sub.add(10); assert list(set_sub) == [10]; assert set_sub.name == "Hello :)""#
);
}
#[pyclass(extends=PyDict)]
#[derive(Debug)]
struct DictWithName {
#[pyo3(get(name))]
#[pyo3(get, name = "name")]
_name: &'static str,
}
@ -206,7 +206,7 @@ mod inheriting_native_type {
py_run!(
py,
dict_sub,
r#"dict_sub[0] = 1; assert dict_sub[0] == 1; assert dict_sub._name == "Hello :)""#
r#"dict_sub[0] = 1; assert dict_sub[0] == 1; assert dict_sub.name == "Hello :)""#
);
}

View File

@ -4,7 +4,7 @@ use pyo3::prelude::*;
use pyo3::types::PyCFunction;
#[cfg(not(Py_LIMITED_API))]
use pyo3::types::{PyDateTime, PyFunction};
use pyo3::{raw_pycfunction, wrap_pyfunction};
use pyo3::wrap_pyfunction;
mod common;
@ -164,31 +164,6 @@ fn test_function_with_custom_conversion_error() {
);
}
#[test]
fn test_raw_function() {
let gil = Python::acquire_gil();
let py = gil.python();
let raw_func = raw_pycfunction!(optional_bool);
let fun = PyCFunction::new_with_keywords(raw_func, "fun", "", py.into()).unwrap();
let res = fun.call((), None).unwrap().extract::<&str>().unwrap();
assert_eq!(res, "Some(true)");
let res = fun.call((false,), None).unwrap().extract::<&str>().unwrap();
assert_eq!(res, "Some(false)");
let no_module = fun.getattr("__module__").unwrap().is_none();
assert!(no_module);
let module = PyModule::new(py, "cool_module").unwrap();
module.add_function(fun).unwrap();
let res = module
.getattr("fun")
.unwrap()
.call((), None)
.unwrap()
.extract::<&str>()
.unwrap();
assert_eq!(res, "Some(true)");
}
#[pyfunction]
fn conversion_error(str_arg: &str, int_arg: i64, tuple_arg: (&str, f64), option_arg: Option<i64>) {
println!(

View File

@ -25,6 +25,18 @@ impl ClassWithSetter {
}
#[pyclass]
struct TupleGetterSetter(#[pyo3(get, set)] i32);
struct TupleGetterSetterNoName(#[pyo3(get, set)] i32);
#[pyclass]
struct MultipleGet(#[pyo3(get, get)] i32);
#[pyclass]
struct MultipleSet(#[pyo3(set, set)] i32);
#[pyclass]
struct MultipleName(#[pyo3(name = "foo", name = "bar")] i32);
#[pyclass]
struct NameWithoutGetSet(#[pyo3(name = "value")] i32);
fn main() {}

View File

@ -16,8 +16,32 @@ error: setter function can have at most two arguments ([pyo3::Python,] and value
24 | fn setter_with_too_many_args(&mut self, py: Python, foo: u32, bar: u32) {}
| ^^^
error: `#[pyo3(get, set)]` is not supported on tuple struct fields
--> $DIR/invalid_property_args.rs:28:44
error: `get` and `set` with tuple struct fields require `name`
--> $DIR/invalid_property_args.rs:28:50
|
28 | struct TupleGetterSetter(#[pyo3(get, set)] i32);
| ^^^
28 | struct TupleGetterSetterNoName(#[pyo3(get, set)] i32);
| ^^^
error: `get` may only be specified once
--> $DIR/invalid_property_args.rs:31:32
|
31 | struct MultipleGet(#[pyo3(get, get)] i32);
| ^^^
error: `set` may only be specified once
--> $DIR/invalid_property_args.rs:34:32
|
34 | struct MultipleSet(#[pyo3(set, set)] i32);
| ^^^
error: `name` may only be specified once
--> $DIR/invalid_property_args.rs:37:49
|
37 | struct MultipleName(#[pyo3(name = "foo", name = "bar")] i32);
| ^^^^^
error: `name` is useless without `get` or `set`
--> $DIR/invalid_property_args.rs:40:40
|
40 | struct NameWithoutGetSet(#[pyo3(name = "value")] i32);
| ^^^^^^^