Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

text_signature: move to #[pyo3(text_signature = "...")] #1658

Merged
merged 1 commit into from Jun 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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