Skip to content

Commit

Permalink
Merge pull request #1658 from davidhewitt/pyo3-text-signature
Browse files Browse the repository at this point in the history
text_signature: move to `#[pyo3(text_signature = "...")]`
  • Loading branch information
davidhewitt committed Jun 5, 2021
2 parents a5810ea + cec4c2d commit 51fbf61
Show file tree
Hide file tree
Showing 19 changed files with 317 additions and 171 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- 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)
- Add `#[pyo3(text_signature = "...")]` syntax for setting text signature. [#1658](https://github.com/PyO3/pyo3/pull/1658)

### 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)
Expand All @@ -49,6 +50,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- 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)
- Deprecate `#[text_signature = "..."]` attributes in favor of `#[pyo3(text_signature = "...")]`. [#1658](https://github.com/PyO3/pyo3/pull/1658)

### Removed
- Remove deprecated exception names `BaseException` etc. [#1426](https://github.com/PyO3/pyo3/pull/1426)
Expand Down
6 changes: 3 additions & 3 deletions guide/src/building_and_distribution.md
Expand Up @@ -41,10 +41,10 @@ There are two main ways to test, build and distribute your module as a Python pa

### Manual builds

You can also symlink (or copy) and rename the shared library from the `target` folder:
You can also symlink (or copy) and rename the shared library from the `target` folder:
- on macOS, rename `libyour_module.dylib` to `your_module.so`.
- on Windows, rename `libyour_module.dll` to `your_module.pyd`.
- on Linux, rename `libyour_module.so` to `your_module.so`.
- on Linux, rename `libyour_module.so` to `your_module.so`.

You can then open a Python shell in the same folder and you'll be able to use `import your_module`.

Expand Down Expand Up @@ -86,7 +86,7 @@ As an advanced feature, you can build PyO3 wheel without calling Python interpre
Due to limitations in the Python API, there are a few `pyo3` features that do
not work when compiling for `abi3`. These are:

- `#[text_signature]` does not work on classes until Python 3.10 or greater.
- `#[pyo3(text_signature = "...")]` does not work on classes until Python 3.10 or greater.
- The `dict` and `weakref` options on classes are not supported until Python 3.9 or greater.
- The buffer API is not supported.
- Optimizations which rely on knowledge of the exact Python version compiled against.
Expand Down
14 changes: 7 additions & 7 deletions guide/src/function.md
Expand Up @@ -117,7 +117,7 @@ fn module_with_functions(py: Python, m: &PyModule) -> PyResult<()> {
## Making the function signature available to Python

In order to make the function signature available to Python to be retrieved via
`inspect.signature`, use the `#[text_signature]` annotation as in the example
`inspect.signature`, use the `#[pyo3(text_signature)]` annotation as in the example
below. The `/` signifies the end of positional-only arguments. (This
is not a feature of this library in particular, but the general format used by
CPython for annotating signatures of built-in functions.)
Expand All @@ -127,7 +127,7 @@ use pyo3::prelude::*;

/// This function adds two unsigned 64-bit integers.
#[pyfunction]
#[text_signature = "(a, b, /)"]
#[pyo3(text_signature = "(a, b, /)")]
fn add(a: u64, b: u64) -> u64 {
a + b
}
Expand All @@ -142,7 +142,7 @@ use pyo3::types::PyType;
// it works even if the item is not documented:

#[pyclass]
#[text_signature = "(c, d, /)"]
#[pyo3(text_signature = "(c, d, /)")]
struct MyClass {}

#[pymethods]
Expand All @@ -154,17 +154,17 @@ impl MyClass {
Self {}
}
// the self argument should be written $self
#[text_signature = "($self, e, f)"]
#[pyo3(text_signature = "($self, e, f)")]
fn my_method(&self, e: i32, f: i32) -> i32 {
e + f
}
#[classmethod]
#[text_signature = "(cls, e, f)"]
#[pyo3(text_signature = "(cls, e, f)")]
fn my_class_method(cls: &PyType, e: i32, f: i32) -> i32 {
e + f
}
#[staticmethod]
#[text_signature = "(e, f)"]
#[pyo3(text_signature = "(e, f)")]
fn my_static_method(e: i32, f: i32) -> i32 {
e + f
}
Expand All @@ -180,7 +180,7 @@ Alternatively, simply make sure the first line of your docstring is
formatted like in the following example. Please note that the newline after the
`--` is mandatory. The `/` signifies the end of positional-only arguments.

`#[text_signature]` should be preferred, since it will override automatically
`#[pyo3(text_signature)]` should be preferred, since it will override automatically
generated signatures when those are added in a future version of PyO3.

```rust
Expand Down
64 changes: 64 additions & 0 deletions pyo3-macros-backend/src/attributes.rs
Expand Up @@ -18,6 +18,7 @@ pub mod kw {
syn::custom_keyword!(name);
syn::custom_keyword!(set);
syn::custom_keyword!(signature);
syn::custom_keyword!(text_signature);
syn::custom_keyword!(transparent);
}

Expand Down Expand Up @@ -45,6 +46,23 @@ impl Parse for NameAttribute {
}
}

#[derive(Clone, Debug, PartialEq)]
pub struct TextSignatureAttribute {
pub kw: kw::text_signature,
pub eq_token: Token![=],
pub lit: LitStr,
}

impl Parse for TextSignatureAttribute {
fn parse(input: ParseStream) -> Result<Self> {
Ok(TextSignatureAttribute {
kw: input.parse()?,
eq_token: input.parse()?,
lit: input.parse()?,
})
}
}

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)
Expand Down Expand Up @@ -112,3 +130,49 @@ pub fn get_deprecated_name_attribute(
_ => Ok(None),
}
}

pub fn get_deprecated_text_signature_attribute(
attr: &syn::Attribute,
deprecations: &mut Deprecations,
) -> syn::Result<Option<TextSignatureAttribute>> {
match attr.parse_meta() {
Ok(syn::Meta::NameValue(syn::MetaNameValue {
path,
lit: syn::Lit::Str(lit),
..
})) if path.is_ident("text_signature") => {
let text_signature = TextSignatureAttribute {
kw: syn::parse_quote!(text_signature),
eq_token: syn::parse_quote!(=),
lit,
};
deprecations.push(
crate::deprecations::Deprecation::TextSignatureAttribute,
attr.span(),
);
Ok(Some(text_signature))
}
_ => Ok(None),
}
}

pub fn take_deprecated_text_signature_attribute(
attrs: &mut Vec<syn::Attribute>,
deprecations: &mut Deprecations,
) -> syn::Result<Option<TextSignatureAttribute>> {
let mut text_signature = None;
let mut attrs_out = Vec::with_capacity(attrs.len());
for attr in attrs.drain(..) {
if let Some(value) = get_deprecated_text_signature_attribute(&attr, deprecations)? {
ensure_spanned!(
text_signature.is_none(),
attr.span() => "text_signature attribute already specified previously"
);
text_signature = Some(value);
} else {
attrs_out.push(attr);
}
}
*attrs = attrs_out;
Ok(text_signature)
}
2 changes: 2 additions & 0 deletions pyo3-macros-backend/src/deprecations.rs
Expand Up @@ -4,13 +4,15 @@ use quote::{quote_spanned, ToTokens};
pub enum Deprecation {
NameAttribute,
PyfnNameArgument,
TextSignatureAttribute,
}

impl Deprecation {
fn ident(&self, span: Span) -> syn::Ident {
let string = match self {
Deprecation::NameAttribute => "NAME_ATTRIBUTE",
Deprecation::PyfnNameArgument => "PYFN_NAME_ARGUMENT",
Deprecation::TextSignatureAttribute => "TEXT_SIGNATURE_ATTRIBUTE",
};
syn::Ident::new(string, span)
}
Expand Down
58 changes: 28 additions & 30 deletions pyo3-macros-backend/src/method.rs
@@ -1,5 +1,6 @@
// Copyright (c) 2017-present PyO3 Project and Contributors

use crate::attributes::TextSignatureAttribute;
use crate::pyfunction::PyFunctionOptions;
use crate::pyfunction::{PyFunctionArgPyO3Attributes, PyFunctionSignature};
use crate::utils;
Expand Down Expand Up @@ -209,13 +210,19 @@ impl<'a> FnSpec<'a> {
}

let (fn_type, skip_first_arg) = Self::parse_fn_type(sig, fn_type_attr, &mut python_name)?;
Self::ensure_text_signature_on_valid_method(&fn_type, options.text_signature.as_ref())?;

let name = &sig.ident;
let ty = get_return_info(&sig.output);
let python_name = python_name.as_ref().unwrap_or(name).unraw();

let text_signature = Self::parse_text_signature(meth_attrs, &fn_type, &python_name)?;
let doc = utils::get_doc(&meth_attrs, text_signature, true)?;
let doc = utils::get_doc(
&meth_attrs,
options
.text_signature
.as_ref()
.map(|attr| (&python_name, attr)),
)?;

let arguments = if skip_first_arg {
sig.inputs
Expand Down Expand Up @@ -246,36 +253,27 @@ impl<'a> FnSpec<'a> {
syn::LitStr::new(&format!("{}\0", self.python_name), self.python_name.span())
}

fn parse_text_signature(
meth_attrs: &mut Vec<syn::Attribute>,
fn ensure_text_signature_on_valid_method(
fn_type: &FnType,
python_name: &syn::Ident,
) -> syn::Result<Option<syn::LitStr>> {
let mut parse_erroneous_text_signature = |error_msg: &str| {
// try to parse anyway to give better error messages
if let Some(text_signature) =
utils::parse_text_signature_attrs(meth_attrs, &python_name)?
{
bail_spanned!(text_signature.span() => error_msg)
} else {
Ok(None)
}
};

let text_signature = match &fn_type {
FnType::Fn(_) | FnType::FnClass | FnType::FnStatic => {
utils::parse_text_signature_attrs(&mut *meth_attrs, &python_name)?
text_signature: Option<&TextSignatureAttribute>,
) -> syn::Result<()> {
if let Some(text_signature) = text_signature {
match &fn_type {
FnType::FnNew => bail_spanned!(
text_signature.kw.span() =>
"text_signature not allowed on __new__; if you want to add a signature on \
__new__, put it on the struct definition instead"
),
FnType::FnCall(_)
| FnType::Getter(_)
| FnType::Setter(_)
| FnType::ClassAttribute => bail_spanned!(
text_signature.kw.span() => "text_signature not allowed with this method type"
),
_ => {}
}
FnType::FnNew => parse_erroneous_text_signature(
"text_signature not allowed on __new__; if you want to add a signature on \
__new__, put it on the struct definition instead",
)?,
FnType::FnCall(_) | FnType::Getter(_) | FnType::Setter(_) | FnType::ClassAttribute => {
parse_erroneous_text_signature("text_signature not allowed with this method type")?
}
};

Ok(text_signature)
}
Ok(())
}

fn parse_fn_type(
Expand Down
3 changes: 2 additions & 1 deletion pyo3-macros-backend/src/module.rs
Expand Up @@ -14,6 +14,7 @@ use syn::{parse::Parse, spanned::Spanned, token::Comma, Ident, Path};
/// module
pub fn py_init(fnname: &Ident, name: &Ident, doc: syn::LitStr) -> TokenStream {
let cb_name = Ident::new(&format!("PyInit_{}", name), Span::call_site());
assert!(doc.value().ends_with('\0'));

quote! {
#[no_mangle]
Expand All @@ -23,7 +24,7 @@ pub fn py_init(fnname: &Ident, name: &Ident, doc: syn::LitStr) -> TokenStream {
pub unsafe extern "C" fn #cb_name() -> *mut pyo3::ffi::PyObject {
use pyo3::derive_utils::ModuleDef;
static NAME: &str = concat!(stringify!(#name), "\0");
static DOC: &str = concat!(#doc, "\0");
static DOC: &str = #doc;
static MODULE_DEF: ModuleDef = unsafe { ModuleDef::new(NAME, DOC) };

pyo3::callback::handle_panic(|_py| { MODULE_DEF.make_module(_py, #fnname) })
Expand Down

0 comments on commit 51fbf61

Please sign in to comment.