Merge pull request #675 from programmerjake/add-text-signature

add #[text_signature = "..."] attribute
This commit is contained in:
Yuji Kanagawa 2019-12-07 13:56:33 +09:00 committed by GitHub
commit 352707c29f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 439 additions and 30 deletions

View file

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## Unreleased
### Added
* Support for `#[text_signature]` attribute. [#675](https://github.com/PyO3/pyo3/pull/675)
## [0.8.3]
### Fixed

View file

@ -74,15 +74,72 @@ fn module_with_functions(py: Python, m: &PyModule) -> PyResult<()> {
# fn main() {}
```
### Making the function signature available to Python
## Making the function signature available to Python
In order to make the function signature available to Python to be retrieved via
`inspect.signature`, simply make sure the first line of your docstring is
formatted like in the example below. Please note that the newline after the
`inspect.signature`, use the `#[text_signature]` annotation as in the example
below. The `/` signifies the end of positional-only arguments.
```rust
use pyo3::prelude::*;
/// This function adds two unsigned 64-bit integers.
#[pyfunction]
#[text_signature = "(a, b, /)"]
fn add(a: u64, b: u64) -> u64 {
a + b
}
```
This also works for classes and methods:
```rust
use pyo3::prelude::*;
use pyo3::types::PyType;
// it works even if the item is not documented:
#[pyclass]
#[text_signature = "(c, d, /)"]
struct MyClass {}
#[pymethods]
impl MyClass {
// the signature for the constructor is attached
// to the struct definition instead.
#[new]
fn new(obj: &PyRawObject, c: i32, d: &str) {
obj.init(Self {});
}
// the self argument should be written $self
#[text_signature = "($self, e, f)"]
fn my_method(&self, e: i32, f: i32) -> i32 {
e + f
}
#[classmethod]
#[text_signature = "(cls, e, f)"]
fn my_class_method(cls: &PyType, e: i32, f: i32) -> i32 {
e + f
}
#[staticmethod]
#[text_signature = "(e, f)"]
fn my_static_method(e: i32, f: i32) -> i32 {
e + f
}
}
```
### Making the function signature available to Python (old method)
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. This
is not a feature of this library in particular, but the general format used by
CPython for annotating signatures of built-in functions.
`#[text_signature]` should be preferred, since it will override automatically
generated signatures when those are added in a future version of PyO3.
```rust
use pyo3::prelude::*;
@ -94,6 +151,17 @@ use pyo3::prelude::*;
fn add(a: u64, b: u64) -> u64 {
a + b
}
// a function with a signature but without docs. Both blank lines after the `--` are mandatory.
/// sub(a, b, /)
/// --
///
///
#[pyfunction]
fn sub(a: u64, b: u64) -> u64 {
a - b
}
```
When annotated like this, signatures are also correctly displayed in IPython.

View file

@ -50,7 +50,7 @@ impl<'a> FnSpec<'a> {
pub fn parse(
name: &'a syn::Ident,
sig: &'a syn::Signature,
meth_attrs: &'a mut Vec<syn::Attribute>,
meth_attrs: &mut Vec<syn::Attribute>,
) -> syn::Result<FnSpec<'a>> {
let (mut fn_type, fn_attrs) = parse_attributes(meth_attrs)?;

View file

@ -133,7 +133,7 @@ fn function_wrapper_ident(name: &Ident) -> Ident {
/// Generates python wrapper over a function that allows adding it to a python module as a python
/// function
pub fn add_fn_to_module(
func: &syn::ItemFn,
func: &mut syn::ItemFn,
python_name: &Ident,
pyfn_attrs: Vec<pyfunction::Argument>,
) -> TokenStream {
@ -157,7 +157,14 @@ pub fn add_fn_to_module(
let function_wrapper_ident = function_wrapper_ident(&func.sig.ident);
let wrapper = function_c_wrapper(&func.sig.ident, &spec);
let doc = utils::get_doc(&func.attrs, true);
let text_signature = match utils::parse_text_signature_attrs(&mut func.attrs, python_name) {
Ok(text_signature) => text_signature,
Err(err) => return err.to_compile_error(),
};
let doc = match utils::get_doc(&func.attrs, text_signature, true) {
Ok(doc) => doc,
Err(err) => return err.to_compile_error(),
};
let tokens = quote! {
fn #function_wrapper_ident(py: pyo3::Python) -> pyo3::PyObject {

View file

@ -157,7 +157,11 @@ impl PyClassArgs {
}
pub fn build_py_class(class: &mut syn::ItemStruct, attr: &PyClassArgs) -> syn::Result<TokenStream> {
let doc = utils::get_doc(&class.attrs, true);
let text_signature = utils::parse_text_signature_attrs(
&mut class.attrs,
&get_class_python_name(&class.ident, attr),
)?;
let doc = utils::get_doc(&class.attrs, text_signature, true)?;
let mut descriptors = Vec::new();
check_generics(class)?;
@ -245,16 +249,20 @@ fn impl_inventory(cls: &syn::Ident) -> TokenStream {
}
}
fn get_class_python_name(cls: &syn::Ident, attr: &PyClassArgs) -> TokenStream {
match &attr.name {
Some(name) => quote! { #name },
None => quote! { #cls },
}
}
fn impl_class(
cls: &syn::Ident,
attr: &PyClassArgs,
doc: syn::Lit,
descriptors: Vec<(syn::Field, Vec<FnType>)>,
) -> TokenStream {
let cls_name = match &attr.name {
Some(name) => quote! { #name }.to_string(),
None => cls.to_string(),
};
let cls_name = get_class_python_name(cls, attr).to_string();
let extra = {
if let Some(freelist) = &attr.freelist {

View file

@ -12,8 +12,50 @@ pub fn gen_py_method(
) -> syn::Result<TokenStream> {
check_generic(name, sig)?;
let doc = utils::get_doc(&meth_attrs, true);
let spec = FnSpec::parse(name, sig, meth_attrs)?;
let spec = FnSpec::parse(name, sig, &mut *meth_attrs)?;
let mut parse_erroneous_text_signature = |alt_name: Option<&str>, error_msg: &str| {
let python_name;
let python_name = match alt_name {
None => name,
Some(alt_name) => {
python_name = syn::Ident::new(alt_name, name.span());
&python_name
}
};
// try to parse anyway to give better error messages
if let Some(text_signature) =
utils::parse_text_signature_attrs(&mut *meth_attrs, python_name)?
{
Err(syn::Error::new_spanned(text_signature, error_msg))
} else {
Ok(None)
}
};
let text_signature = match &spec.tp {
FnType::Fn | FnType::PySelf(_) | FnType::FnClass | FnType::FnStatic => {
utils::parse_text_signature_attrs(&mut *meth_attrs, name)?
}
FnType::FnNew => parse_erroneous_text_signature(
Some("__new__"),
"text_signature not allowed on __new__; if you want to add a signature on \
__new__, put it on the struct definition instead",
)?,
FnType::FnCall => parse_erroneous_text_signature(
Some("__call__"),
"text_signature not allowed on __call__",
)?,
FnType::Getter(getter_name) => parse_erroneous_text_signature(
getter_name.as_ref().map(|v| &**v),
"text_signature not allowed on getter",
)?,
FnType::Setter(setter_name) => parse_erroneous_text_signature(
setter_name.as_ref().map(|v| &**v),
"text_signature not allowed on setter",
)?,
};
let doc = utils::get_doc(&meth_attrs, text_signature, true)?;
Ok(match spec.tp {
FnType::Fn => impl_py_method_def(name, doc, &spec, &impl_wrap(cls, name, &spec, true)),

View file

@ -2,6 +2,7 @@
use proc_macro2::Span;
use proc_macro2::TokenStream;
use std::fmt::Display;
pub fn print_err(msg: String, t: TokenStream) {
println!("Error: {} in '{}'", msg, t.to_string());
@ -20,35 +21,123 @@ pub fn if_type_is_python(ty: &syn::Type) -> bool {
}
}
// FIXME(althonos): not sure the docstring formatting is on par here.
pub fn get_doc(attrs: &[syn::Attribute], null_terminated: bool) -> syn::Lit {
let mut doc = Vec::new();
pub fn is_text_signature_attr(attr: &syn::Attribute) -> bool {
attr.path.is_ident("text_signature")
}
// TODO(althonos): set span on produced doc str literal
// let mut span = None;
fn parse_text_signature_attr<T: Display + quote::ToTokens + ?Sized>(
attr: &syn::Attribute,
python_name: &T,
) -> syn::Result<Option<syn::LitStr>> {
if !is_text_signature_attr(attr) {
return Ok(None);
}
let python_name_str = python_name.to_string();
let python_name_str = python_name_str
.rsplit('.')
.next()
.map(str::trim)
.filter(|v| !v.is_empty())
.ok_or_else(|| {
syn::Error::new_spanned(
&python_name,
format!("failed to parse python name: {}", python_name),
)
})?;
match attr.parse_meta()? {
syn::Meta::NameValue(syn::MetaNameValue {
lit: syn::Lit::Str(lit),
..
}) => {
let value = lit.value();
if value.starts_with('(') && value.ends_with(')') {
Ok(Some(syn::LitStr::new(
&(python_name_str.to_owned() + &value),
lit.span(),
)))
} else {
Err(syn::Error::new_spanned(
lit,
"text_signature must start with \"(\" and end with \")\"",
))
}
}
meta => Err(syn::Error::new_spanned(
meta,
"text_signature must be of the form #[text_signature = \"\"]",
)),
}
}
pub fn parse_text_signature_attrs<T: Display + quote::ToTokens + ?Sized>(
attrs: &mut Vec<syn::Attribute>,
python_name: &T,
) -> syn::Result<Option<syn::LitStr>> {
let mut text_signature = None;
let mut attrs_out = Vec::with_capacity(attrs.len());
for attr in attrs.drain(..) {
if let Some(value) = parse_text_signature_attr(&attr, python_name)? {
if text_signature.is_some() {
return Err(syn::Error::new_spanned(
attr,
"text_signature attribute already specified previously",
));
} else {
text_signature = Some(value);
}
} else {
attrs_out.push(attr);
}
}
*attrs = attrs_out;
Ok(text_signature)
}
// FIXME(althonos): not sure the docstring formatting is on par here.
pub fn get_doc(
attrs: &[syn::Attribute],
text_signature: Option<syn::LitStr>,
null_terminated: bool,
) -> syn::Result<syn::Lit> {
let mut doc = String::new();
let mut span = Span::call_site();
if let Some(text_signature) = text_signature {
// create special doc string lines to set `__text_signature__`
span = text_signature.span();
doc.push_str(&text_signature.value());
doc.push_str("\n--\n\n");
}
let mut separator = "";
let mut first = true;
for attr in attrs.iter() {
if let Ok(syn::Meta::NameValue(ref metanv)) = attr.parse_meta() {
if metanv.path.is_ident("doc") {
// span = Some(metanv.span());
if let syn::Lit::Str(ref litstr) = metanv.lit {
if first {
first = false;
span = litstr.span();
}
let d = litstr.value();
doc.push(if d.starts_with(' ') {
d[1..d.len()].to_string()
doc.push_str(separator);
if d.starts_with(' ') {
doc.push_str(&d[1..d.len()]);
} else {
d
});
doc.push_str(&d);
};
separator = "\n";
} else {
panic!("Invalid doc comment");
return Err(syn::Error::new_spanned(metanv, "Invalid doc comment"));
}
}
}
}
let mut docstr = doc.join("\n");
if null_terminated {
docstr.push('\0');
doc.push('\0');
}
syn::Lit::Str(syn::LitStr::new(&docstr, Span::call_site()))
Ok(syn::Lit::Str(syn::LitStr::new(&doc, span)))
}

View file

@ -27,7 +27,12 @@ pub fn pymodule(attr: TokenStream, input: TokenStream) -> TokenStream {
process_functions_in_module(&mut ast);
let expanded = py_init(&ast.sig.ident, &modname, get_doc(&ast.attrs, false));
let doc = match get_doc(&ast.attrs, None, false) {
Ok(doc) => doc,
Err(err) => return err.to_compile_error().into(),
};
let expanded = py_init(&ast.sig.ident, &modname, doc);
quote!(
#ast
@ -75,11 +80,11 @@ pub fn pymethods(_: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro_attribute]
pub fn pyfunction(attr: TokenStream, input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as syn::ItemFn);
let mut ast = parse_macro_input!(input as syn::ItemFn);
let args = parse_macro_input!(attr as PyFunctionAttr);
let python_name = syn::Ident::new(&ast.sig.ident.unraw().to_string(), Span::call_site());
let expanded = add_fn_to_module(&ast, &python_name, args.arguments);
let expanded = add_fn_to_module(&mut ast, &python_name, args.arguments);
quote!(
#ast

View file

@ -0,0 +1,186 @@
use pyo3::prelude::*;
use pyo3::{types::PyType, wrap_pyfunction, wrap_pymodule};
mod common;
#[test]
fn class_without_docs_or_signature() {
#[pyclass]
struct MyClass {}
let gil = Python::acquire_gil();
let py = gil.python();
let typeobj = py.get_type::<MyClass>();
py_assert!(py, typeobj, "typeobj.__doc__ is None");
py_assert!(py, typeobj, "typeobj.__text_signature__ is None");
}
#[test]
fn class_with_docs() {
/// docs line1
#[pyclass]
/// docs line2
struct MyClass {}
let gil = Python::acquire_gil();
let py = gil.python();
let typeobj = py.get_type::<MyClass>();
py_assert!(py, typeobj, "typeobj.__doc__ == 'docs line1\\ndocs line2'");
py_assert!(py, typeobj, "typeobj.__text_signature__ is None");
}
#[test]
fn class_with_docs_and_signature() {
/// docs line1
#[pyclass]
/// docs line2
#[text_signature = "(a, b=None, *, c=42)"]
/// docs line3
struct MyClass {}
#[pymethods]
impl MyClass {
#[new]
#[args(a, b = "None", "*", c = 42)]
fn __new__(obj: &PyRawObject, a: i32, b: Option<i32>, c: i32) {
let _ = (a, b, c);
obj.init(Self {});
}
}
let gil = Python::acquire_gil();
let py = gil.python();
let typeobj = py.get_type::<MyClass>();
py_assert!(
py,
typeobj,
"typeobj.__doc__ == 'docs line1\\ndocs line2\\ndocs line3'"
);
py_assert!(
py,
typeobj,
"typeobj.__text_signature__ == '(a, b=None, *, c=42)'"
);
}
#[test]
fn class_with_signature() {
#[pyclass]
#[text_signature = "(a, b=None, *, c=42)"]
struct MyClass {}
#[pymethods]
impl MyClass {
#[new]
#[args(a, b = "None", "*", c = 42)]
fn __new__(obj: &PyRawObject, a: i32, b: Option<i32>, c: i32) {
let _ = (a, b, c);
obj.init(Self {});
}
}
let gil = Python::acquire_gil();
let py = gil.python();
let typeobj = py.get_type::<MyClass>();
py_assert!(py, typeobj, "typeobj.__doc__ is None");
py_assert!(
py,
typeobj,
"typeobj.__text_signature__ == '(a, b=None, *, c=42)'"
);
}
#[test]
fn test_function() {
#[pyfunction(a, b = "None", "*", c = 42)]
#[text_signature = "(a, b=None, *, c=42)"]
fn my_function(a: i32, b: Option<i32>, c: i32) {
let _ = (a, b, c);
}
let gil = Python::acquire_gil();
let py = gil.python();
let f = wrap_pyfunction!(my_function)(py);
py_assert!(py, f, "f.__text_signature__ == '(a, b=None, *, c=42)'");
}
#[test]
fn test_pyfn() {
#[pymodule]
fn my_module(_py: Python, m: &PyModule) -> PyResult<()> {
#[pyfn(m, "my_function", a, b = "None", "*", c = 42)]
#[text_signature = "(a, b=None, *, c=42)"]
fn my_function(a: i32, b: Option<i32>, c: i32) {
let _ = (a, b, c);
}
Ok(())
}
let gil = Python::acquire_gil();
let py = gil.python();
let m = wrap_pymodule!(my_module)(py);
py_assert!(
py,
m,
"m.my_function.__text_signature__ == '(a, b=None, *, c=42)'"
);
}
#[test]
fn test_methods() {
#[pyclass]
struct MyClass {}
#[pymethods]
impl MyClass {
#[text_signature = "($self, a)"]
fn method(&self, a: i32) {
let _ = a;
}
#[text_signature = "($self, b)"]
fn pyself_method(_this: PyRef<Self>, b: i32) {
let _ = b;
}
#[classmethod]
#[text_signature = "($cls, c)"]
fn class_method(_cls: &PyType, c: i32) {
let _ = c;
}
#[staticmethod]
#[text_signature = "(d)"]
fn static_method(d: i32) {
let _ = d;
}
}
let gil = Python::acquire_gil();
let py = gil.python();
let typeobj = py.get_type::<MyClass>();
py_assert!(
py,
typeobj,
"typeobj.method.__text_signature__ == '($self, a)'"
);
py_assert!(
py,
typeobj,
"typeobj.pyself_method.__text_signature__ == '($self, b)'"
);
py_assert!(
py,
typeobj,
"typeobj.class_method.__text_signature__ == '($cls, c)'"
);
py_assert!(
py,
typeobj,
"typeobj.static_method.__text_signature__ == '(d)'"
);
}