Skip to content

Commit

Permalink
Merge pull request #1610 from davidhewitt/pyfn-name
Browse files Browse the repository at this point in the history
pyfn: deprecate name argument
  • Loading branch information
davidhewitt authored May 23, 2021
2 parents 7b72dd7 + a109640 commit cfdd535
Show file tree
Hide file tree
Showing 21 changed files with 256 additions and 160 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `PyAsyncProtocol::__aenter__` and `PyAsyncProtocol::__aexit__`
- Deprecate `#[name = "..."]` attributes in favor of `#[pyo3(name = "...")]`. [#1567](https://github.com/PyO3/pyo3/pull/1567)
- Improve compilation times for projects using PyO3 [#1604](https://github.com/PyO3/pyo3/pull/1604)
- Deprecate string-literal second argument to `#[pyfn(m, "name")]`.

### Removed
- Remove deprecated exception names `BaseException` etc. [#1426](https://github.com/PyO3/pyo3/pull/1426)
Expand Down
2 changes: 0 additions & 2 deletions guide/src/class/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,6 @@ These correspond to the slots `tp_traverse` and `tp_clear` in the Python C API.
as every cycle must contain at least one mutable reference.
Example:
```rust
extern crate pyo3;

use pyo3::prelude::*;
use pyo3::PyTraverseError;
use pyo3::gc::{PyGCProtocol, PyVisit};
Expand Down
106 changes: 64 additions & 42 deletions guide/src/function.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
# Python Functions

PyO3 supports two ways to define a free function in Python. Both require registering
the function to a [module](./module.md).
PyO3 supports two ways to define a free function in Python. Both require registering the function to a [module](./module.md).

One way is defining the function in the module definition, annotated with `#[pyfn]`.
One way is annotating a function with `#[pyfunction]` and then adding it to the module using the `wrap_pyfunction!` macro.

```rust
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

#[pyfunction]
fn double(x: usize) -> usize {
x * 2
}

#[pymodule]
fn module_with_functions(py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(double, m)?)?;
Ok(())
}

# fn main() {}
```

Alternatively there is a shorthand; the function can be placed inside the module definition and annotated with `#[pyfn]`, as below:

```rust
use pyo3::prelude::*;

#[pymodule]
fn rust2py(py: Python, m: &PyModule) -> PyResult<()> {

#[pyfn(m, "sum_as_string")]
fn sum_as_string_py(_py: Python, a:i64, b:i64) -> PyResult<String> {
#[pyfn(m)]
fn sum_as_string(_py: Python, a:i64, b:i64) -> PyResult<String> {
Ok(format!("{}", a + b))
}

Expand All @@ -22,38 +41,61 @@ fn rust2py(py: Python, m: &PyModule) -> PyResult<()> {
# fn main() {}
```

The other is annotating a function with `#[pyfunction]` and then adding it
to the module using the `wrap_pyfunction!` macro.
`#[pyfn(m)]` is just syntax sugar for `#[pyfunction]`, and takes all the same options documented in the rest of this chapter. The code above is expanded to the following:

```rust
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

#[pyfunction]
fn double(x: usize) -> usize {
x * 2
}

#[pymodule]
fn module_with_functions(py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(double, m)?).unwrap();
fn rust2py(py: Python, m: &PyModule) -> PyResult<()> {

#[pyfunction]
fn sum_as_string(_py: Python, a:i64, b:i64) -> PyResult<String> {
Ok(format!("{}", a + b))
}

m.add_function(pyo3::wrap_pyfunction!(sum_as_string, m)?)?;

Ok(())
}

# fn main() {}
```

## Function options

The `#[pyo3]` attribute can be used to modify properties of the generated Python function. It can take any combination of the following options:

- `#[pyo3(name = "...")]`

Overrides the name generated in Python:

```rust
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

#[pyfunction]
#[pyo3(name = "no_args")]
fn no_args_py() -> usize { 42 }

#[pymodule]
fn module_with_functions(py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(no_args_py, m)?)?;
Ok(())
}

# Python::with_gil(|py| {
# let m = pyo3::wrap_pymodule!(module_with_functions)(py);
# assert!(m.getattr(py, "no_args").is_ok());
# assert!(m.getattr(py, "no_args_py").is_err());
# });
```

## Argument parsing

Both the `#[pyfunction]` and `#[pyfn]` attributes support specifying details of
argument parsing. The details are given in the section "Method arguments" in
the [Classes](class.md) chapter. Here is an example for a function that accepts
arbitrary keyword arguments (`**kwargs` in Python syntax) and returns the number
that was passed:
The `#[pyfunction]` attribute supports specifying details of argument parsing. The details are given in the section ["Method arguments" of the Classes chapter](class.md#method-arguments). Here is an example for a function that accepts arbitrary keyword arguments (`**kwargs` in Python syntax) and returns the number that was passed:

```rust
# extern crate pyo3;
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;
use pyo3::types::PyDict;
Expand Down Expand Up @@ -213,8 +255,7 @@ in Python code.

### Accessing the module of a function

It is possible to access the module of a `#[pyfunction]` and `#[pyfn]` in the
function body by passing the `pass_module` argument to the attribute:
It is possible to access the module of a `#[pyfunction]` in the function body by passing the `pass_module` argument to the attribute:

```rust
use pyo3::wrap_pyfunction;
Expand All @@ -236,25 +277,6 @@ fn module_with_fn(py: Python, m: &PyModule) -> PyResult<()> {
If `pass_module` is set, the first argument **must** be the `&PyModule`. It is then possible to use the module
in the function body.

The same works for `#[pyfn]`:

```rust
use pyo3::wrap_pyfunction;
use pyo3::prelude::*;

#[pymodule]
fn module_with_fn(py: Python, m: &PyModule) -> PyResult<()> {

#[pyfn(m, "module_name", pass_module)]
fn module_name(module: &PyModule) -> PyResult<&str> {
module.name()
}
Ok(())
}

# fn main() {}
```

## Accessing the FFI functions

In order to make Rust functions callable from Python, PyO3 generates a
Expand Down
9 changes: 5 additions & 4 deletions guide/src/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ fn rust2py(py: Python, m: &PyModule) -> PyResult<()> {
// Note that the `#[pyfn()]` annotation automatically converts the arguments from
// Python objects to Rust values, and the Rust return value back into a Python object.
// The `_py` argument represents that we're holding the GIL.
#[pyfn(m, "sum_as_string")]
#[pyfn(m)]
#[pyo3(name = "sum_as_string")]
fn sum_as_string_py(_py: Python, a: i64, b: i64) -> PyResult<String> {
let out = sum_as_string(a, b);
Ok(out)
Expand All @@ -32,7 +33,7 @@ fn sum_as_string(a: i64, b: i64) -> String {
# fn main() {}
```

The `#[pymodule]` procedural macro attribute takes care of exporting the initialization function of your
The `#[pymodule]` procedural macro attribute takes care of exporting the initialization function of your
module to Python. It can take as an argument the name of your module, which must be the name of the `.so`
or `.pyd` file; the default is the Rust function's name.

Expand All @@ -41,7 +42,7 @@ If the name of the module (the default being the function name) does not match t
`ImportError: dynamic module does not define module export function (PyInit_name_of_your_module)`

To import the module, either copy the shared library as described in [the README](https://github.com/PyO3/pyo3)
or use a tool, e.g. `maturin develop` with [maturin](https://github.com/PyO3/maturin) or
or use a tool, e.g. `maturin develop` with [maturin](https://github.com/PyO3/maturin) or
`python setup.py develop` with [setuptools-rust](https://github.com/PyO3/setuptools-rust).

## Documentation
Expand Down Expand Up @@ -95,4 +96,4 @@ fn supermodule(py: Python, module: &PyModule) -> PyResult<()> {

This way, you can create a module hierarchy within a single extension module.

It is not necessary to add `#[pymodule]` on nested modules, this is only required on the top-level module.
It is not necessary to add `#[pymodule]` on nested modules, this is only required on the top-level module.
13 changes: 8 additions & 5 deletions pyo3-macros-backend/src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use syn::{
Attribute, ExprPath, Ident, LitStr, Result, Token,
};

use crate::deprecations::{Deprecation, Deprecations};

pub mod kw {
syn::custom_keyword!(annotation);
syn::custom_keyword!(attribute);
Expand Down Expand Up @@ -81,17 +83,18 @@ pub fn take_attributes(
Ok(())
}

pub fn get_deprecated_name_attribute(attr: &syn::Attribute) -> syn::Result<Option<NameAttribute>> {
pub fn get_deprecated_name_attribute(
attr: &syn::Attribute,
deprecations: &mut Deprecations,
) -> syn::Result<Option<NameAttribute>> {
match attr.parse_meta() {
Ok(syn::Meta::NameValue(syn::MetaNameValue {
path,
lit: syn::Lit::Str(s),
..
})) if path.is_ident("name") => {
let mut ident: syn::Ident = s.parse()?;
// This span is the whole attribute span, which is nicer for reporting errors.
ident.set_span(attr.span());
Ok(Some(NameAttribute(ident)))
deprecations.push(Deprecation::NameAttribute, attr.span());
Ok(Some(NameAttribute(s.parse()?)))
}
_ => Ok(None),
}
Expand Down
43 changes: 43 additions & 0 deletions pyo3-macros-backend/src/deprecations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use proc_macro2::{Span, TokenStream};
use quote::{quote_spanned, ToTokens};

pub enum Deprecation {
NameAttribute,
PyfnNameArgument,
}

impl Deprecation {
fn ident(&self, span: Span) -> syn::Ident {
let string = match self {
Deprecation::NameAttribute => "NAME_ATTRIBUTE",
Deprecation::PyfnNameArgument => "PYFN_NAME_ARGUMENT",
};
syn::Ident::new(string, span)
}
}

#[derive(Default)]
pub struct Deprecations(Vec<(Deprecation, Span)>);

impl Deprecations {
pub fn new() -> Self {
Deprecations(Vec::new())
}

pub fn push(&mut self, deprecation: Deprecation, span: Span) {
self.0.push((deprecation, span))
}
}

impl ToTokens for Deprecations {
fn to_tokens(&self, tokens: &mut TokenStream) {
for (deprecation, span) in &self.0 {
let ident = deprecation.ident(*span);
quote_spanned!(
*span =>
let _ = pyo3::impl_::deprecations::#ident;
)
.to_tokens(tokens)
}
}
}
25 changes: 13 additions & 12 deletions pyo3-macros-backend/src/konst.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::attributes::{
self, get_deprecated_name_attribute, get_pyo3_attributes, is_attribute_ident, take_attributes,
NameAttribute,
use crate::{
attributes::{
self, get_deprecated_name_attribute, get_pyo3_attributes, is_attribute_ident,
take_attributes, NameAttribute,
},
deprecations::Deprecations,
};
use crate::utils;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{
Expand All @@ -19,12 +21,10 @@ pub struct ConstSpec {

impl ConstSpec {
/// Null-terminated Python name
pub fn python_name_with_deprecation(&self) -> TokenStream {
pub fn null_terminated_python_name(&self) -> TokenStream {
if let Some(name) = &self.attributes.name {
let deprecation =
utils::name_deprecation_token(name.0.span(), self.attributes.name_is_deprecated);
let name = format!("{}\0", name.0);
quote!({#deprecation #name})
quote!({#name})
} else {
let name = format!("{}\0", self.rust_ident.unraw().to_string());
quote!(#name)
Expand All @@ -35,7 +35,7 @@ impl ConstSpec {
pub struct ConstAttributes {
pub is_class_attr: bool,
pub name: Option<NameAttribute>,
pub name_is_deprecated: bool,
pub deprecations: Deprecations,
}

pub enum PyO3ConstAttribute {
Expand All @@ -58,7 +58,7 @@ impl ConstAttributes {
let mut attributes = ConstAttributes {
is_class_attr: false,
name: None,
name_is_deprecated: false,
deprecations: Deprecations::new(),
};

take_attributes(attrs, |attr| {
Expand All @@ -76,9 +76,10 @@ impl ConstAttributes {
}
}
Ok(true)
} else if let Some(name) = get_deprecated_name_attribute(attr)? {
} else if let Some(name) =
get_deprecated_name_attribute(attr, &mut attributes.deprecations)?
{
attributes.set_name(name)?;
attributes.name_is_deprecated = true;
Ok(true)
} else {
Ok(false)
Expand Down
1 change: 1 addition & 0 deletions pyo3-macros-backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod utils;

mod attributes;
mod defs;
mod deprecations;
mod from_pyobject;
mod konst;
mod method;
Expand Down
Loading

0 comments on commit cfdd535

Please sign in to comment.