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

add #[text_signature = "..."] attribute #675

Merged
merged 8 commits into from
Dec 7, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pyo3-derive-backend/src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ pub fn get_return_info(output: &syn::ReturnType) -> syn::Type {

impl<'a> FnSpec<'a> {
/// Parser function signature and function attributes
pub fn parse(
pub fn parse<'b>(
programmerjake marked this conversation as resolved.
Show resolved Hide resolved
name: &'a syn::Ident,
sig: &'a syn::Signature,
meth_attrs: &'a mut Vec<syn::Attribute>,
meth_attrs: &'b mut Vec<syn::Attribute>,
) -> syn::Result<FnSpec<'a>> {
let (mut fn_type, fn_attrs) = parse_attributes(meth_attrs)?;

Expand Down
11 changes: 9 additions & 2 deletions pyo3-derive-backend/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) {
programmerjake marked this conversation as resolved.
Show resolved Hide resolved
Ok(text_signature) => text_signature,
Err(err) => return err.to_compile_error(),
};
let doc = match utils::get_doc(&func.attrs, text_signature, true) {
programmerjake marked this conversation as resolved.
Show resolved Hide resolved
Ok(doc) => doc,
Err(err) => return err.to_compile_error(),
};

let tokens = quote! {
fn #function_wrapper_ident(py: pyo3::Python) -> pyo3::PyObject {
Expand Down
18 changes: 13 additions & 5 deletions pyo3-derive-backend/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down Expand Up @@ -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);

let extra = {
if let Some(freelist) = &attr.freelist {
Expand Down
34 changes: 32 additions & 2 deletions pyo3-derive-backend/src/pymethod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,38 @@ 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 text_signature = match &spec.tp {
FnType::Fn | FnType::PySelf(_) | FnType::FnClass | FnType::FnStatic => {
utils::parse_text_signature_attrs(&mut *meth_attrs, name)?
}
FnType::FnNew => utils::parse_text_signature_attrs(
&mut *meth_attrs,
&syn::Ident::new("__new__", name.span()),
)?,
FnType::FnCall => utils::parse_text_signature_attrs(
&mut *meth_attrs,
&syn::Ident::new("__call__", name.span()),
)?,
FnType::Getter(get_set_name) | FnType::Setter(get_set_name) => {
// try to parse anyway to give better error messages
let get_set_name = match get_set_name {
None => name.clone(),
Some(get_set_name) => syn::Ident::new(get_set_name, name.span()),
};
if let Some(type_signature) =
utils::parse_text_signature_attrs(&mut *meth_attrs, &get_set_name)?
{
return Err(syn::Error::new_spanned(
type_signature,
"type_signature not allowed on getters/setters",
));
}
None
}
};
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)),
Expand Down
111 changes: 108 additions & 3 deletions pyo3-derive-backend/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -20,9 +21,108 @@ pub fn if_type_is_python(ty: &syn::Type) -> bool {
}
}

pub fn is_text_signature_attr(attr: &syn::Attribute) -> bool {
attr.path.is_ident("text_signature")
}

fn parse_text_signature_attr<T: Display + quote::ToTokens + ?Sized>(
attr: &syn::Attribute,
python_name: &T,
text_signature: &mut Option<syn::LitStr>,
) -> syn::Result<Option<()>> {
if !is_text_signature_attr(attr) {
return Ok(None);
}
if text_signature.is_some() {
return Err(syn::Error::new_spanned(
attr,
"text_signature attribute already specified previously",
));
}
let value: String;
match attr.parse_meta()? {
syn::Meta::NameValue(syn::MetaNameValue {
lit: syn::Lit::Str(lit),
..
}) => {
value = lit.value();
*text_signature = Some(lit);
}
meta => {
return Err(syn::Error::new_spanned(
meta,
"text_signature must be of the form #[text_signature = \"\"]",
));
}
};
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),
)
})?;
if !value.starts_with(&python_name_str) || !value[python_name_str.len()..].starts_with('(') {
return Err(syn::Error::new_spanned(
text_signature,
format!("text_signature must start with \"{}(\"", python_name_str),
));
}
if !value.ends_with(')') {
return Err(syn::Error::new_spanned(
text_signature,
"text_signature must end with \")\"",
));
}
Ok(Some(()))
}

pub fn parse_text_signature_attrs<T: Display + quote::ToTokens + ?Sized>(
attrs: &mut Vec<syn::Attribute>,
programmerjake marked this conversation as resolved.
Show resolved Hide resolved
python_name: &T,
) -> syn::Result<Option<syn::LitStr>> {
let mut parse_error: Option<syn::Error> = None;
let mut text_signature = None;
attrs.retain(|attr| {
match parse_text_signature_attr(attr, python_name, &mut text_signature) {
Ok(None) => return true,
Ok(Some(_)) => {}
Err(err) => {
if let Some(parse_error) = &mut parse_error {
programmerjake marked this conversation as resolved.
Show resolved Hide resolved
parse_error.combine(err);
} else {
parse_error = Some(err);
}
}
}
false
});
if let Some(parse_error) = parse_error {
return Err(parse_error);
}
Ok(text_signature)
}

// FIXME(althonos): not sure the docstring formatting is on par here.
pub fn get_doc(attrs: &[syn::Attribute], null_terminated: bool) -> syn::Lit {
pub fn get_doc(
attrs: &[syn::Attribute],
text_signature: Option<syn::LitStr>,
null_terminated: bool,
) -> syn::Result<syn::Lit> {
let mut doc = Vec::new();
let mut needs_terminating_newline = false;

if let Some(text_signature) = text_signature {
doc.push(text_signature.value());
doc.push("--".to_string());
doc.push(String::new());
needs_terminating_newline = true;
}

// TODO(althonos): set span on produced doc str literal
// let mut span = None;
Expand All @@ -38,17 +138,22 @@ pub fn get_doc(attrs: &[syn::Attribute], null_terminated: bool) -> syn::Lit {
} else {
d
});
needs_terminating_newline = false;
} else {
panic!("Invalid doc comment");
return Err(syn::Error::new_spanned(metanv, "Invalid doc comment"));
}
}
}
}

if needs_terminating_newline {
doc.push(String::new());
}

let mut docstr = doc.join("\n");
if null_terminated {
docstr.push('\0');
}

syn::Lit::Str(syn::LitStr::new(&docstr, Span::call_site()))
Ok(syn::Lit::Str(syn::LitStr::new(&docstr, Span::call_site())))
}
11 changes: 8 additions & 3 deletions pyo3cls/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
programmerjake marked this conversation as resolved.
Show resolved Hide resolved
Ok(doc) => doc,
Err(err) => return err.to_compile_error().into(),
};

let expanded = py_init(&ast.sig.ident, &modname, doc);

quote!(
#ast
Expand Down Expand Up @@ -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
Expand Down