From 8acb2241917abf34a5e6ee9869453f894af95dfa Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Tue, 7 Feb 2023 21:15:32 +0000 Subject: [PATCH] support `text_signature` on `#[new]` --- guide/src/class.md | 27 +-- guide/src/class/numeric.md | 6 +- guide/src/function/signature.md | 4 +- newsfragments/2980.added.md | 1 + newsfragments/2980.changed.md | 1 + pyo3-macros-backend/src/deprecations.rs | 2 + pyo3-macros-backend/src/method.rs | 34 +++- pyo3-macros-backend/src/pyclass.rs | 55 ++++--- pyo3-macros-backend/src/pyfunction.rs | 44 +---- .../src/pyfunction/signature.rs | 15 +- pyo3-macros-backend/src/pymethod.rs | 38 +++-- pyo3-macros-backend/src/utils.rs | 30 ++-- src/impl_/deprecations.rs | 6 + src/impl_/pyclass.rs | 47 +++++- src/impl_/pymethods.rs | 26 +-- src/internal_tricks.rs | 36 +++- src/pyclass/create_type_object.rs | 59 +++---- tests/test_text_signature.rs | 155 +++++++++++++++--- tests/ui/invalid_pymethods.rs | 7 - tests/ui/invalid_pymethods.stderr | 91 +++++----- 20 files changed, 409 insertions(+), 275 deletions(-) create mode 100644 newsfragments/2980.added.md create mode 100644 newsfragments/2980.changed.md diff --git a/guide/src/class.md b/guide/src/class.md index 2f02c25f2af..e209c2e335d 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -722,22 +722,20 @@ py_args=(), py_kwargs=None, name=World, num=-1, num_before=44 ## Making class method signatures available to Python -The [`text_signature = "..."`](./function.md#text_signature) option for `#[pyfunction]` also works for classes and methods: +The [`text_signature = "..."`](./function.md#text_signature) option for `#[pyfunction]` also works for `#[pymethods]`: ```rust # #![allow(dead_code)] use pyo3::prelude::*; use pyo3::types::PyType; -// it works even if the item is not documented: -#[pyclass(text_signature = "(c, d, /)")] +#[pyclass] struct MyClass {} #[pymethods] impl MyClass { - // the signature for the constructor is attached - // to the struct definition instead. #[new] + #[pyo3(text_signature = "(c, d)")] fn new(c: i32, d: &str) -> Self { Self {} } @@ -746,8 +744,9 @@ impl MyClass { fn my_method(&self, e: i32, f: i32) -> i32 { e + f } + // similarly for classmethod arguments, use $cls #[classmethod] - #[pyo3(text_signature = "(cls, e, f)")] + #[pyo3(text_signature = "($cls, e, f)")] fn my_class_method(cls: &PyType, e: i32, f: i32) -> i32 { e + f } @@ -773,7 +772,7 @@ impl MyClass { # .call1((class,))? # .call_method0("__str__")? # .extract()?; -# assert_eq!(sig, "(c, d, /)"); +# assert_eq!(sig, "(c, d)"); # } else { # let doc: String = class.getattr("__doc__")?.extract()?; # assert_eq!(doc, ""); @@ -802,7 +801,7 @@ impl MyClass { # .call1((method,))? # .call_method0("__str__")? # .extract()?; -# assert_eq!(sig, "(cls, e, f)"); +# assert_eq!(sig, "(e, f)"); // inspect.signature skips the $cls arg # } # # { @@ -822,7 +821,7 @@ impl MyClass { # } ``` -Note that `text_signature` on classes is not compatible with compilation in +Note that `text_signature` on `#[new]` is not compatible with compilation in `abi3` mode until Python 3.10 or greater. ## #[pyclass] enums @@ -1018,7 +1017,6 @@ impl pyo3::IntoPy for MyClass { } impl pyo3::impl_::pyclass::PyClassImpl for MyClass { - const DOC: &'static str = "Class for demonstration\u{0}"; const IS_BASETYPE: bool = false; const IS_SUBCLASS: bool = false; type Layout = PyCell; @@ -1041,6 +1039,15 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass { static TYPE_OBJECT: LazyTypeObject = LazyTypeObject::new(); &TYPE_OBJECT } + + fn doc(py: Python<'_>) -> pyo3::PyResult<&'static ::std::ffi::CStr> { + use pyo3::impl_::pyclass::*; + static DOC: pyo3::once_cell::GILOnceCell<::std::borrow::Cow<'static, ::std::ffi::CStr>> = pyo3::once_cell::GILOnceCell::new(); + DOC.get_or_try_init(py, || { + let collector = PyClassImplCollector::::new(); + build_pyclass_doc(::NAME, "", None.or_else(|| collector.new_text_signature())) + }).map(::std::ops::Deref::deref) + } } # Python::with_gil(|py| { diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index 532be94fe29..21327ae24b5 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -42,7 +42,7 @@ fn wrap(obj: &PyAny) -> Result { Ok(val as i32) } ``` -We also add documentation, via `///` comments and the `#[pyo3(text_signature = "...")]` attribute, both of which are visible to Python users. +We also add documentation, via `///` comments, which are visible to Python users. ```rust # #![allow(dead_code)] @@ -57,7 +57,6 @@ fn wrap(obj: &PyAny) -> Result { /// Did you ever hear the tragedy of Darth Signed The Overfloweth? I thought not. /// It's not a story C would tell you. It's a Rust legend. #[pyclass(module = "my_module")] -#[pyo3(text_signature = "(int)")] struct Number(i32); #[pymethods] @@ -223,7 +222,6 @@ fn wrap(obj: &PyAny) -> Result { /// Did you ever hear the tragedy of Darth Signed The Overfloweth? I thought not. /// It's not a story C would tell you. It's a Rust legend. #[pyclass(module = "my_module")] -#[pyo3(text_signature = "(int)")] struct Number(i32); #[pymethods] @@ -377,7 +375,7 @@ fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { # assert Number(12345234523452) == Number(1498514748) # try: # import inspect -# assert inspect.signature(Number).__str__() == '(int)' +# assert inspect.signature(Number).__str__() == '(value)' # except ValueError: # # Not supported with `abi3` before Python 3.10 # pass diff --git a/guide/src/function/signature.md b/guide/src/function/signature.md index 2908232f621..a3a24ec9549 100644 --- a/guide/src/function/signature.md +++ b/guide/src/function/signature.md @@ -272,9 +272,7 @@ impl MyClass { The function signature is exposed to Python via the `__text_signature__` attribute. PyO3 automatically generates this for every `#[pyfunction]` and all `#[pymethods]` directly from the Rust function, taking into account any override done with the `#[pyo3(signature = (...))]` option. -This automatic generation has some limitations, which may be improved in the future: -- It will not include the value of default arguments, replacing them all with `...`. (`.pyi` type stub files commonly also use `...` for all default arguments in the same way.) -- Nothing is generated for the `#[new]` method of a `#[pyclass]`. +This automatic generation can only display the value of default arguments for strings, integers, boolean types, and `None`. Any other default arguments will be displayed as `...`. (`.pyi` type stub files commonly also use `...` for default arguments in the same way.) In cases where the automatically-generated signature needs adjusting, it can [be overridden](#overriding-the-generated-signature) using the `#[pyo3(text_signature)]` option.) diff --git a/newsfragments/2980.added.md b/newsfragments/2980.added.md new file mode 100644 index 00000000000..ce4b8428fa6 --- /dev/null +++ b/newsfragments/2980.added.md @@ -0,0 +1 @@ +Support `text_signature` option (and automatically generate signature) for `#[new]` in `#[pymethods]`. diff --git a/newsfragments/2980.changed.md b/newsfragments/2980.changed.md new file mode 100644 index 00000000000..38d006bd62e --- /dev/null +++ b/newsfragments/2980.changed.md @@ -0,0 +1 @@ +Deprecate `text_signature` option on `#[pyclass]`. diff --git a/pyo3-macros-backend/src/deprecations.rs b/pyo3-macros-backend/src/deprecations.rs index 9b168f7746f..4ea9c5d8b4a 100644 --- a/pyo3-macros-backend/src/deprecations.rs +++ b/pyo3-macros-backend/src/deprecations.rs @@ -2,6 +2,7 @@ use proc_macro2::{Span, TokenStream}; use quote::{quote_spanned, ToTokens}; pub enum Deprecation { + PyClassTextSignature, PyFunctionArguments, PyMethodArgsAttribute, RequiredArgumentAfterOption, @@ -10,6 +11,7 @@ pub enum Deprecation { impl Deprecation { fn ident(&self, span: Span) -> syn::Ident { let string = match self { + Deprecation::PyClassTextSignature => "PYCLASS_TEXT_SIGNATURE", Deprecation::PyFunctionArguments => "PYFUNCTION_ARGUMENTS", Deprecation::PyMethodArgsAttribute => "PYMETHODS_ARGS_ATTRIBUTE", Deprecation::RequiredArgumentAfterOption => "REQUIRED_ARGUMENT_AFTER_OPTION", diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index e0498fe182f..01aedf695a6 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -1,6 +1,6 @@ // Copyright (c) 2017-present PyO3 Project and Contributors -use crate::attributes::TextSignatureAttribute; +use crate::attributes::{TextSignatureAttribute, TextSignatureAttributeValue}; use crate::deprecations::{Deprecation, Deprecations}; use crate::params::impl_arg_params; use crate::pyfunction::{DeprecatedArgs, FunctionSignature, PyFunctionArgPyO3Attributes}; @@ -593,6 +593,33 @@ impl<'a> FnSpec<'a> { CallingConvention::TpNew => unreachable!("tp_new cannot get a methoddef"), } } + + /// Forwards to [utils::get_doc] with the text signature of this spec. + pub fn get_doc(&self, attrs: &[syn::Attribute]) -> PythonDoc { + let text_signature = self + .text_signature_call_signature() + .map(|sig| format!("{}{}", self.python_name, sig)); + utils::get_doc(attrs, text_signature) + } + + /// Creates the parenthesised arguments list for `__text_signature__` snippet based on this spec's signature + /// and/or attributes. Prepend the callable name to make a complete `__text_signature__`. + pub fn text_signature_call_signature(&self) -> Option { + let self_argument = match &self.tp { + // Getters / Setters / ClassAttribute are not callables on the Python side + FnType::Getter(_) | FnType::Setter(_) | FnType::ClassAttribute => return None, + FnType::Fn(_) => Some("self"), + FnType::FnModule => Some("module"), + FnType::FnClass => Some("cls"), + FnType::FnStatic | FnType::FnNew => None, + }; + + match self.text_signature.as_ref().map(|attr| &attr.value) { + Some(TextSignatureAttributeValue::Str(s)) => Some(s.value()), + None => Some(self.signature.text_signature(self_argument)), + Some(TextSignatureAttributeValue::Disabled(_)) => None, + } + } } #[derive(Debug)] @@ -755,11 +782,6 @@ fn ensure_signatures_on_valid_method( } 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::Getter(_) => { bail_spanned!(text_signature.kw.span() => "`text_signature` not allowed with `getter`") } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index bb8bd7f0cf3..7e7ac6b615f 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -5,11 +5,11 @@ use std::borrow::Cow; use crate::attributes::{ self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute, ModuleAttribute, NameAttribute, NameLitStr, TextSignatureAttribute, + TextSignatureAttributeValue, }; -use crate::deprecations::Deprecations; +use crate::deprecations::{Deprecation, Deprecations}; use crate::konst::{ConstAttributes, ConstSpec}; use crate::method::FnSpec; -use crate::pyfunction::text_signature_or_none; use crate::pyimpl::{gen_py_const, PyClassMethodsType}; use crate::pymethod::{ impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType, @@ -177,7 +177,11 @@ impl PyClassPyO3Options { PyClassPyO3Option::Sequence(sequence) => set_option!(sequence), PyClassPyO3Option::SetAll(set_all) => set_option!(set_all), PyClassPyO3Option::Subclass(subclass) => set_option!(subclass), - PyClassPyO3Option::TextSignature(text_signature) => set_option!(text_signature), + PyClassPyO3Option::TextSignature(text_signature) => { + self.deprecations + .push(Deprecation::PyClassTextSignature, text_signature.span()); + set_option!(text_signature) + } PyClassPyO3Option::Unsendable(unsendable) => set_option!(unsendable), PyClassPyO3Option::Weakref(weakref) => set_option!(weakref), } @@ -191,11 +195,7 @@ pub fn build_py_class( methods_type: PyClassMethodsType, ) -> syn::Result { args.options.take_pyo3_options(&mut class.attrs)?; - let text_signature_string = text_signature_or_none(args.options.text_signature.as_ref()); - let doc = utils::get_doc( - &class.attrs, - text_signature_string.map(|s| (get_class_python_name(&class.ident, &args), s)), - ); + let doc = utils::get_doc(&class.attrs, None); let krate = get_pyo3_crate(&args.options.krate); if let Some(lt) = class.generics.lifetimes().next() { @@ -452,12 +452,7 @@ pub fn build_py_enum( bail_spanned!(enum_.brace_token.span => "#[pyclass] can't be used on enums without any variants"); } - let text_signature_string = text_signature_or_none(args.options.text_signature.as_ref()); - - let doc = utils::get_doc( - &enum_.attrs, - text_signature_string.map(|s| (get_class_python_name(&enum_.ident, &args), s)), - ); + let doc = utils::get_doc(&enum_.attrs, None); let enum_ = PyClassEnum::new(enum_)?; impl_enum(enum_, &args, doc, method_type) } @@ -509,16 +504,6 @@ fn impl_enum( methods_type: PyClassMethodsType, ) -> Result { let krate = get_pyo3_crate(&args.options.krate); - impl_enum_class(enum_, args, doc, methods_type, krate) -} - -fn impl_enum_class( - enum_: PyClassEnum<'_>, - args: &PyClassArgs, - doc: PythonDoc, - methods_type: PyClassMethodsType, - krate: syn::Path, -) -> Result { let cls = enum_.ident; let ty: syn::Type = syn::parse_quote!(#cls); let variants = enum_.variants; @@ -889,6 +874,18 @@ impl<'a> PyClassImplsBuilder<'a> { fn impl_pyclassimpl(&self) -> Result { let cls = self.cls; let doc = self.doc.as_ref().map_or(quote! {"\0"}, |doc| quote! {#doc}); + let deprecated_text_signature = match self + .attr + .options + .text_signature + .as_ref() + .map(|attr| &attr.value) + { + Some(TextSignatureAttributeValue::Str(s)) => quote!(::std::option::Option::Some(#s)), + Some(TextSignatureAttributeValue::Disabled(_)) | None => { + quote!(::std::option::Option::None) + } + }; let is_basetype = self.attr.options.subclass.is_some(); let base = self .attr @@ -1009,7 +1006,6 @@ impl<'a> PyClassImplsBuilder<'a> { Ok(quote! { impl _pyo3::impl_::pyclass::PyClassImpl for #cls { - const DOC: &'static str = #doc; const IS_BASETYPE: bool = #is_basetype; const IS_SUBCLASS: bool = #is_subclass; const IS_MAPPING: bool = #is_mapping; @@ -1035,6 +1031,15 @@ impl<'a> PyClassImplsBuilder<'a> { PyClassItemsIter::new(&INTRINSIC_ITEMS, #pymethods_items) } + fn doc(py: _pyo3::Python<'_>) -> _pyo3::PyResult<&'static ::std::ffi::CStr> { + use _pyo3::impl_::pyclass::*; + static DOC: _pyo3::once_cell::GILOnceCell<::std::borrow::Cow<'static, ::std::ffi::CStr>> = _pyo3::once_cell::GILOnceCell::new(); + DOC.get_or_try_init(py, || { + let collector = PyClassImplCollector::::new(); + build_pyclass_doc(<#cls as _pyo3::PyTypeInfo>::NAME, #doc, #deprecated_text_signature.or_else(|| collector.new_text_signature())) + }).map(::std::ops::Deref::deref) + } + #dict_offset #weaklist_offset diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index d0a5b6c1f18..8f56b3e916a 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -1,16 +1,14 @@ // Copyright (c) 2017-present PyO3 Project and Contributors -use std::borrow::Cow; - use crate::{ attributes::{ self, get_pyo3_options, take_attributes, take_pyo3_options, CrateAttribute, - FromPyWithAttribute, NameAttribute, TextSignatureAttribute, TextSignatureAttributeValue, + FromPyWithAttribute, NameAttribute, TextSignatureAttribute, }, deprecations::{Deprecation, Deprecations}, - method::{self, CallingConvention, FnArg, FnType}, + method::{self, CallingConvention, FnArg}, pymethod::check_generic, - utils::{self, ensure_not_async_fn, get_pyo3_crate}, + utils::{ensure_not_async_fn, get_pyo3_crate}, }; use proc_macro2::TokenStream; use quote::{format_ident, quote}; @@ -409,15 +407,6 @@ pub fn impl_wrap_pyfunction( let ty = method::get_return_info(&func.sig.output); - let text_signature_string = text_signature_or_auto(text_signature.as_ref(), &signature, &tp); - - let doc = utils::get_doc( - &func.attrs, - text_signature_string.map(|s| (Cow::Borrowed(&python_name), s)), - ); - - let krate = get_pyo3_crate(&krate); - let spec = method::FnSpec { tp, name: &func.sig.ident, @@ -430,12 +419,14 @@ pub fn impl_wrap_pyfunction( unsafety: func.sig.unsafety, }; + let krate = get_pyo3_crate(&krate); + let vis = &func.vis; let name = &func.sig.ident; let wrapper_ident = format_ident!("__pyfunction_{}", spec.name); let wrapper = spec.get_wrapper_function(&wrapper_ident, None)?; - let methoddef = spec.get_methoddef(wrapper_ident, &doc); + let methoddef = spec.get_methoddef(wrapper_ident, &spec.get_doc(&func.attrs)); let wrapped_pyfunction = quote! { @@ -480,26 +471,3 @@ fn type_is_pymodule(ty: &syn::Type) -> bool { } false } - -/// Helper to get a text signature string, or None if unset or disabled -pub(crate) fn text_signature_or_none( - text_signature: Option<&TextSignatureAttribute>, -) -> Option { - match text_signature.map(|attr| &attr.value) { - Some(TextSignatureAttributeValue::Str(s)) => Some(s.value()), - Some(TextSignatureAttributeValue::Disabled(_)) | None => None, - } -} - -/// Helper to get a text signature string, using automatic generation if unset, or None if disabled -pub(crate) fn text_signature_or_auto( - text_signature: Option<&TextSignatureAttribute>, - signature: &FunctionSignature<'_>, - fn_type: &FnType, -) -> Option { - match text_signature.map(|attr| &attr.value) { - Some(TextSignatureAttributeValue::Str(s)) => Some(s.value()), - None => Some(signature.text_signature(fn_type)), - Some(TextSignatureAttributeValue::Disabled(_)) => None, - } -} diff --git a/pyo3-macros-backend/src/pyfunction/signature.rs b/pyo3-macros-backend/src/pyfunction/signature.rs index 02f0291c221..b7c9965e0da 100644 --- a/pyo3-macros-backend/src/pyfunction/signature.rs +++ b/pyo3-macros-backend/src/pyfunction/signature.rs @@ -13,7 +13,7 @@ use syn::{ use crate::{ attributes::{kw, KeywordAttribute}, deprecations::{Deprecation, Deprecations}, - method::{FnArg, FnType}, + method::FnArg, pyfunction::Argument, }; @@ -623,18 +623,7 @@ impl<'a> FunctionSignature<'a> { default } - pub fn text_signature(&self, fn_type: &FnType) -> String { - // automatic text signature generation - let self_argument = match fn_type { - FnType::FnNew | FnType::Getter(_) | FnType::Setter(_) | FnType::ClassAttribute => { - unreachable!() - } - FnType::Fn(_) => Some("self"), - FnType::FnModule => Some("module"), - FnType::FnClass => Some("cls"), - FnType::FnStatic => None, - }; - + pub fn text_signature(&self, self_argument: Option<&str>) -> String { let mut output = String::new(); output.push('('); diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 381d250262e..686f0798311 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -4,7 +4,6 @@ use std::borrow::Cow; use crate::attributes::NameAttribute; use crate::method::{CallingConvention, ExtractErrorMode}; -use crate::pyfunction::text_signature_or_auto; use crate::utils::{ensure_not_async_fn, PythonDoc}; use crate::{deprecations::Deprecations, utils}; use crate::{ @@ -219,19 +218,19 @@ pub fn gen_py_method( (_, FnType::Fn(_)) => GeneratedPyMethod::Method(impl_py_method_def( cls, spec, - &create_doc(meth_attrs, spec), + &spec.get_doc(meth_attrs), None, )?), (_, FnType::FnClass) => GeneratedPyMethod::Method(impl_py_method_def( cls, spec, - &create_doc(meth_attrs, spec), + &spec.get_doc(meth_attrs), Some(quote!(_pyo3::ffi::METH_CLASS)), )?), (_, FnType::FnStatic) => GeneratedPyMethod::Method(impl_py_method_def( cls, spec, - &create_doc(meth_attrs, spec), + &spec.get_doc(meth_attrs), Some(quote!(_pyo3::ffi::METH_STATIC)), )?), // special prototypes @@ -242,7 +241,7 @@ pub fn gen_py_method( PropertyType::Function { self_type, spec, - doc: create_doc(meth_attrs, spec), + doc: spec.get_doc(meth_attrs), }, )?), (_, FnType::Setter(self_type)) => GeneratedPyMethod::Method(impl_py_setter_def( @@ -250,7 +249,7 @@ pub fn gen_py_method( PropertyType::Function { self_type, spec, - doc: create_doc(meth_attrs, spec), + doc: spec.get_doc(meth_attrs), }, )?), (_, FnType::FnModule) => { @@ -259,18 +258,6 @@ pub fn gen_py_method( }) } -fn create_doc(meth_attrs: &[syn::Attribute], spec: &FnSpec<'_>) -> PythonDoc { - let text_signature_string = match &spec.tp { - FnType::FnNew | FnType::Getter(_) | FnType::Setter(_) | FnType::ClassAttribute => None, - _ => text_signature_or_auto(spec.text_signature.as_ref(), &spec.signature, &spec.tp), - }; - - utils::get_doc( - meth_attrs, - text_signature_string.map(|sig| (Cow::Borrowed(&spec.python_name), sig)), - ) -} - pub fn check_generic(sig: &syn::Signature) -> syn::Result<()> { let err_msg = |typ| format!("Python functions cannot have generic {} parameters", typ); for param in &sig.generics.params { @@ -335,6 +322,13 @@ pub fn impl_py_method_def( fn impl_py_method_def_new(cls: &syn::Type, spec: &FnSpec<'_>) -> Result { let wrapper_ident = syn::Ident::new("__pymethod___new____", Span::call_site()); let associated_method = spec.get_wrapper_function(&wrapper_ident, Some(cls))?; + // Use just the text_signature_call_signature() because the class' Python name + // isn't known to `#[pymethods]` - that has to be attached at runtime from the PyClassImpl + // trait implementation created by `#[pyclass]`. + let text_signature_body = spec.text_signature_call_signature().map_or_else( + || quote!(::std::option::Option::None), + |text_signature| quote!(::std::option::Option::Some(#text_signature)), + ); let slot_def = quote! { _pyo3::ffi::PyType_Slot { slot: _pyo3::ffi::Py_tp_new, @@ -345,6 +339,14 @@ fn impl_py_method_def_new(cls: &syn::Type, spec: &FnSpec<'_>) -> Result *mut _pyo3::ffi::PyObject { + use _pyo3::impl_::pyclass::*; + impl PyClassNewTextSignature<#cls> for PyClassImplCollector<#cls> { + #[inline] + fn new_text_signature(self) -> ::std::option::Option<&'static str> { + #text_signature_body + } + } + _pyo3::impl_::trampoline::newfunc( subtype, args, diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 03a1d2a6bbf..c9e02d85fb7 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -1,9 +1,7 @@ -use std::{borrow::Cow, fmt::Write}; - // Copyright (c) 2017-present PyO3 Project and Contributors use proc_macro2::{Span, TokenStream}; use quote::ToTokens; -use syn::{punctuated::Punctuated, spanned::Spanned, Ident, Token}; +use syn::{punctuated::Punctuated, spanned::Spanned, Token}; use crate::attributes::CrateAttribute; @@ -67,24 +65,20 @@ pub fn option_type_argument(ty: &syn::Type) -> Option<&syn::Type> { pub struct PythonDoc(TokenStream); /// Collects all #[doc = "..."] attributes into a TokenStream evaluating to a null-terminated string. -pub fn get_doc( - attrs: &[syn::Attribute], - text_signature: Option<(Cow<'_, Ident>, String)>, -) -> PythonDoc { - let mut parts = Punctuated::::new(); - let mut current_part = String::new(); - - if let Some((python_name, text_signature)) = text_signature { - // create special doc string lines to set `__text_signature__` - write!( - &mut current_part, - "{}{}\n--\n\n", - python_name, text_signature - ) - .expect("error occurred while trying to format text_signature to string") +/// +/// If this doc is for a callable, the provided `text_signature` can be passed to prepend +/// this to the documentation suitable for Python to extract this into the `__text_signature__` +/// attribute. +pub fn get_doc(attrs: &[syn::Attribute], mut text_signature: Option) -> PythonDoc { + // insert special divider between `__text_signature__` and doc + // (assume text_signature is itself well-formed) + if let Some(text_signature) = &mut text_signature { + text_signature.push_str("\n--\n\n"); } + let mut parts = Punctuated::::new(); let mut first = true; + let mut current_part = text_signature.unwrap_or_default(); for attr in attrs.iter() { if attr.path.is_ident("doc") { diff --git a/src/impl_/deprecations.rs b/src/impl_/deprecations.rs index 901bea5006a..e6b8268fc07 100644 --- a/src/impl_/deprecations.rs +++ b/src/impl_/deprecations.rs @@ -17,3 +17,9 @@ pub const PYMETHODS_ARGS_ATTRIBUTE: () = (); note = "required arguments after an `Option<_>` argument are ambiguous and being phased out\n= help: add a `#[pyo3(signature)]` annotation on this function to unambiguously specify the default values for all optional parameters" )] pub const REQUIRED_ARGUMENT_AFTER_OPTION: () = (); + +#[deprecated( + since = "0.19.0", + note = "put `text_signature` on `#[new]` instead of `#[pyclass]`" +)] +pub const PYCLASS_TEXT_SIGNATURE: () = (); diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 5430bae2c25..56472e3e2a1 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1,14 +1,17 @@ use crate::{ - exceptions::{PyAttributeError, PyNotImplementedError}, + exceptions::{PyAttributeError, PyNotImplementedError, PyValueError}, ffi, impl_::freelist::FreeList, impl_::pycell::{GetBorrowChecker, PyClassMutability}, + internal_tricks::extract_c_string, pycell::PyCellLayout, pyclass_init::PyObjectInit, type_object::PyLayout, Py, PyAny, PyCell, PyClass, PyErr, PyMethodDefType, PyNativeType, PyResult, PyTypeInfo, Python, }; use std::{ + borrow::Cow, + ffi::{CStr, CString}, marker::PhantomData, os::raw::{c_int, c_void}, ptr::NonNull, @@ -138,9 +141,6 @@ unsafe impl Sync for PyClassItems {} /// Users are discouraged from implementing this trait manually; it is a PyO3 implementation detail /// and may be changed at any time. pub trait PyClassImpl: Sized + 'static { - /// Class doc string - const DOC: &'static str = "\0"; - /// #[pyclass(subclass)] const IS_BASETYPE: bool = false; @@ -184,12 +184,16 @@ pub trait PyClassImpl: Sized + 'static { #[cfg(feature = "multiple-pymethods")] type Inventory: PyClassInventory; + /// Rendered class doc + fn doc(py: Python<'_>) -> PyResult<&'static CStr>; + fn items_iter() -> PyClassItemsIter; #[inline] fn dict_offset() -> Option { None } + #[inline] fn weaklist_offset() -> Option { None @@ -198,6 +202,29 @@ pub trait PyClassImpl: Sized + 'static { fn lazy_type_object() -> &'static LazyTypeObject; } +/// Runtime helper to build a class docstring from the `doc` and `text_signature`. +/// +/// This is done at runtime because the class text signature is collected via dtolnay +/// specialization in to the `#[pyclass]` macro from the `#[pymethods]` macro. +pub fn build_pyclass_doc( + class_name: &'static str, + doc: &'static str, + text_signature: Option<&'static str>, +) -> PyResult> { + if let Some(text_signature) = text_signature { + let doc = CString::new(format!( + "{}{}\n--\n\n{}", + class_name, + text_signature, + doc.trim_end_matches('\0') + )) + .map_err(|_| PyValueError::new_err("class doc cannot contain nul bytes"))?; + Ok(Cow::Owned(doc)) + } else { + extract_c_string(doc, "class doc cannot contain nul bytes") + } +} + /// Iterator used to process all class items during type instantiation. pub struct PyClassItemsIter { /// Iteration state @@ -840,6 +867,18 @@ impl PyMethods for &'_ PyClassImplCollector { } } +// Text signature for __new__ +pub trait PyClassNewTextSignature { + fn new_text_signature(self) -> Option<&'static str>; +} + +impl PyClassNewTextSignature for &'_ PyClassImplCollector { + #[inline] + fn new_text_signature(self) -> Option<&'static str> { + None + } +} + // Thread checkers #[doc(hidden)] diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 36da7765637..c6b3fe2e94d 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -1,7 +1,7 @@ -use crate::exceptions::PyValueError; +use crate::internal_tricks::extract_c_string; use crate::{ffi, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, PyTraverseError, Python}; use std::borrow::Cow; -use std::ffi::{CStr, CString}; +use std::ffi::CStr; use std::fmt; use std::os::raw::c_int; @@ -319,25 +319,3 @@ where self.map(|o| o.into_py(py)) } } - -fn extract_c_string(src: &'static str, err_msg: &'static str) -> PyResult> { - let bytes = src.as_bytes(); - let cow = match bytes { - [] => { - // Empty string, we can trivially refer to a static "\0" string - Cow::Borrowed(unsafe { CStr::from_bytes_with_nul_unchecked(b"\0") }) - } - [.., 0] => { - // Last byte is a nul; try to create as a CStr - let c_str = - CStr::from_bytes_with_nul(bytes).map_err(|_| PyValueError::new_err(err_msg))?; - Cow::Borrowed(c_str) - } - _ => { - // Allocate a new CString for this - let c_string = CString::new(bytes).map_err(|_| PyValueError::new_err(err_msg))?; - Cow::Owned(c_string) - } - }; - Ok(cow) -} diff --git a/src/internal_tricks.rs b/src/internal_tricks.rs index c6a59aa8c81..551ddf5a910 100644 --- a/src/internal_tricks.rs +++ b/src/internal_tricks.rs @@ -1,4 +1,13 @@ -use crate::ffi::{Py_ssize_t, PY_SSIZE_T_MAX}; +use std::{ + borrow::Cow, + ffi::{CStr, CString}, +}; + +use crate::{ + exceptions::PyValueError, + ffi::{Py_ssize_t, PY_SSIZE_T_MAX}, + PyResult, +}; pub struct PrivateMarker; macro_rules! private_decl { @@ -178,3 +187,28 @@ pub(crate) fn slice_end_index_len_fail(index: usize, ty_name: &str, len: usize) pub(crate) fn slice_index_order_fail(index: usize, end: usize) -> ! { panic!("slice index starts at {} but ends at {}", index, end); } + +pub(crate) fn extract_c_string( + src: &'static str, + err_msg: &'static str, +) -> PyResult> { + let bytes = src.as_bytes(); + let cow = match bytes { + [] => { + // Empty string, we can trivially refer to a static "\0" string + Cow::Borrowed(unsafe { CStr::from_bytes_with_nul_unchecked(b"\0") }) + } + [.., 0] => { + // Last byte is a nul; try to create as a CStr + let c_str = + CStr::from_bytes_with_nul(bytes).map_err(|_| PyValueError::new_err(err_msg))?; + Cow::Borrowed(c_str) + } + _ => { + // Allocate a new CString for this + let c_string = CString::new(bytes).map_err(|_| PyValueError::new_err(err_msg))?; + Cow::Owned(c_string) + } + }; + Ok(cow) +} diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index 882f3a5dbe0..29e09739d94 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -22,7 +22,7 @@ where { unsafe { PyTypeBuilder::default() - .type_doc(T::DOC) + .type_doc(T::doc(py)?) .offsets(T::dict_offset(), T::weaklist_offset()) .slot(ffi::Py_tp_base, T::BaseType::type_object_raw(py)) .slot(ffi::Py_tp_dealloc, tp_dealloc:: as *mut c_void) @@ -233,25 +233,26 @@ impl PyTypeBuilder { self } - fn type_doc(mut self, type_doc: &'static str) -> Self { - if let Some(doc) = py_class_doc(type_doc) { - unsafe { self.push_slot(ffi::Py_tp_doc, doc) } - } - - // Running this causes PyPy to segfault. - #[cfg(all(not(PyPy), not(Py_LIMITED_API), not(Py_3_10)))] - if type_doc != "\0" { - // Until CPython 3.10, tp_doc was treated specially for - // heap-types, and it removed the text_signature value from it. - // We go in after the fact and replace tp_doc with something - // that _does_ include the text_signature value! - self.cleanup - .push(Box::new(move |_self, type_object| unsafe { - ffi::PyObject_Free((*type_object).tp_doc as _); - let data = ffi::PyObject_Malloc(type_doc.len()); - data.copy_from(type_doc.as_ptr() as _, type_doc.len()); - (*type_object).tp_doc = data as _; - })) + fn type_doc(mut self, type_doc: &'static CStr) -> Self { + let slice = type_doc.to_bytes(); + if !slice.is_empty() { + unsafe { self.push_slot(ffi::Py_tp_doc, type_doc.as_ptr() as *mut c_char) } + + // Running this causes PyPy to segfault. + #[cfg(all(not(PyPy), not(Py_LIMITED_API), not(Py_3_10)))] + { + // Until CPython 3.10, tp_doc was treated specially for + // heap-types, and it removed the text_signature value from it. + // We go in after the fact and replace tp_doc with something + // that _does_ include the text_signature value! + self.cleanup + .push(Box::new(move |_self, type_object| unsafe { + ffi::PyObject_Free((*type_object).tp_doc as _); + let data = ffi::PyMem_Malloc(slice.len()); + data.copy_from(slice.as_ptr() as _, slice.len()); + (*type_object).tp_doc = data as _; + })) + } } self } @@ -383,24 +384,6 @@ impl PyTypeBuilder { } } -fn py_class_doc(class_doc: &str) -> Option<*mut c_char> { - match class_doc { - "\0" => None, - s => { - // To pass *mut pointer to python safely, leak a CString in whichever case - let cstring = if s.as_bytes().last() == Some(&0) { - CStr::from_bytes_with_nul(s.as_bytes()) - .unwrap_or_else(|e| panic!("doc contains interior nul byte: {:?} in {}", e, s)) - .to_owned() - } else { - CString::new(s) - .unwrap_or_else(|e| panic!("doc contains interior nul byte: {:?} in {}", e, s)) - }; - Some(cstring.into_raw()) - } - } -} - fn py_class_qualified_name(module_name: Option<&str>, class_name: &str) -> PyResult<*mut c_char> { Ok(CString::new(format!( "{}.{}", diff --git a/tests/test_text_signature.rs b/tests/test_text_signature.rs index 937c4ec445c..03a16961f73 100644 --- a/tests/test_text_signature.rs +++ b/tests/test_text_signature.rs @@ -36,18 +36,14 @@ fn class_with_docs() { #[test] #[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)] -fn class_with_docs_and_signature() { - /// docs line1 +fn class_with_signature_no_doc() { #[pyclass] - /// docs line2 - #[pyo3(text_signature = "(a, b=None, *, c=42)")] - /// docs line3 struct MyClass {} #[pymethods] impl MyClass { #[new] - #[pyo3(signature = (a, b=None, *, c=42))] + #[pyo3(signature = (a, b=None, *, c=42), text_signature = "(a, b=None, *, c=42)")] fn __new__(a: i32, b: Option, c: i32) -> Self { let _ = (a, b, c); Self {} @@ -56,12 +52,7 @@ fn class_with_docs_and_signature() { Python::with_gil(|py| { let typeobj = py.get_type::(); - - py_assert!( - py, - typeobj, - "typeobj.__doc__ == 'docs line1\\ndocs line2\\ndocs line3'" - ); + py_assert!(py, typeobj, "typeobj.__doc__ == ''"); py_assert!( py, typeobj, @@ -72,15 +63,16 @@ fn class_with_docs_and_signature() { #[test] #[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)] -fn class_with_signature() { +fn class_with_docs_and_signature() { + /// docs line1 #[pyclass] - #[pyo3(text_signature = "(a, b=None, *, c=42)")] + /// docs line2 struct MyClass {} #[pymethods] impl MyClass { #[new] - #[pyo3(signature = (a, b=None, *, c=42))] + #[pyo3(signature = (a, b=None, *, c=42), text_signature = "(a, b=None, *, c=42)")] fn __new__(a: i32, b: Option, c: i32) -> Self { let _ = (a, b, c); Self {} @@ -90,11 +82,7 @@ fn class_with_signature() { Python::with_gil(|py| { let typeobj = py.get_type::(); - py_assert!( - py, - typeobj, - "typeobj.__doc__ is None or typeobj.__doc__ == ''" - ); + py_assert!(py, typeobj, "typeobj.__doc__ == 'docs line1\\ndocs line2'"); py_assert!( py, typeobj, @@ -209,6 +197,12 @@ fn test_auto_test_signature_method() { #[pymethods] impl MyClass { + #[new] + fn new(a: i32, b: i32, c: i32) -> Self { + let _ = (a, b, c); + Self {} + } + fn method(&self, a: i32, b: i32, c: i32) { let _ = (a, b, c); } @@ -244,6 +238,7 @@ fn test_auto_test_signature_method() { Python::with_gil(|py| { let cls = py.get_type::(); + py_assert!(py, cls, "cls.__text_signature__ == '(a, b, c)'"); py_assert!( py, cls, @@ -289,6 +284,13 @@ fn test_auto_test_signature_opt_out() { #[pymethods] impl MyClass { + #[new] + #[pyo3(text_signature = None)] + fn new(a: i32, b: i32, c: i32) -> Self { + let _ = (a, b, c); + Self {} + } + #[pyo3(text_signature = None)] fn method(&self, a: i32, b: i32, c: i32) { let _ = (a, b, c); @@ -320,6 +322,7 @@ fn test_auto_test_signature_opt_out() { py_assert!(py, f, "f.__text_signature__ == None"); let cls = py.get_type::(); + py_assert!(py, cls, "cls.__text_signature__ == None"); py_assert!(py, cls, "cls.method.__text_signature__ == None"); py_assert!(py, cls, "cls.method_2.__text_signature__ == None"); py_assert!(py, cls, "cls.staticmethod.__text_signature__ == None"); @@ -407,7 +410,6 @@ fn test_methods() { #[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)] fn test_raw_identifiers() { #[pyclass] - #[pyo3(text_signature = "($self)")] struct r#MyClass {} #[pymethods] @@ -416,14 +418,13 @@ fn test_raw_identifiers() { fn new() -> MyClass { MyClass {} } - #[pyo3(text_signature = "($self)")] fn r#method(&self) {} } Python::with_gil(|py| { let typeobj = py.get_type::(); - py_assert!(py, typeobj, "typeobj.__text_signature__ == '($self)'"); + py_assert!(py, typeobj, "typeobj.__text_signature__ == '()'"); py_assert!( py, @@ -432,3 +433,111 @@ fn test_raw_identifiers() { ); }); } + +#[allow(deprecated)] +mod deprecated { + use crate::py_assert; + use pyo3::prelude::*; + + #[test] + #[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)] + fn class_with_docs_and_signature() { + /// docs line1 + #[pyclass] + /// docs line2 + #[pyo3(text_signature = "(a, b=None, *, c=42)")] + /// docs line3 + struct MyClass {} + + #[pymethods] + impl MyClass { + #[new] + #[pyo3(signature = (a, b=None, *, c=42))] + fn __new__(a: i32, b: Option, c: i32) -> Self { + let _ = (a, b, c); + Self {} + } + } + + Python::with_gil(|py| { + let typeobj = py.get_type::(); + + 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] + #[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)] + fn class_with_deprecated_text_signature() { + #[pyclass] + #[pyo3(text_signature = "(a, b=None, *, c=42)")] + struct MyClass {} + + #[pymethods] + impl MyClass { + #[new] + #[pyo3(signature = (a, b=None, *, c=42))] + fn __new__(a: i32, b: Option, c: i32) -> Self { + let _ = (a, b, c); + Self {} + } + } + + Python::with_gil(|py| { + let typeobj = py.get_type::(); + + py_assert!( + py, + typeobj, + "typeobj.__doc__ is None or typeobj.__doc__ == ''" + ); + py_assert!( + py, + typeobj, + "typeobj.__text_signature__ == '(a, b=None, *, c=42)'" + ); + }); + } + + #[test] + #[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)] + fn class_with_deprecated_text_signature_and_on_new() { + #[pyclass(text_signature = "(a, b=None, *, c=42)")] + struct MyClass {} + + #[pymethods] + impl MyClass { + #[new] + #[pyo3(signature = (a, b=None, *, c=42), text_signature = "(NOT, THIS, ONE)")] + fn __new__(a: i32, b: Option, c: i32) -> Self { + let _ = (a, b, c); + Self {} + } + } + + Python::with_gil(|py| { + let typeobj = py.get_type::(); + py_assert!( + py, + typeobj, + "typeobj.__doc__ is None or typeobj.__doc__ == ''" + ); + // Deprecated `#[pyclass(text_signature)]` attribute will be preferred + // for backwards-compatibility. + py_assert!( + py, + typeobj, + "typeobj.__text_signature__ == '(a, b=None, *, c=42)'" + ); + }); + } +} diff --git a/tests/ui/invalid_pymethods.rs b/tests/ui/invalid_pymethods.rs index a6690b05ae9..b2a183a2f7e 100644 --- a/tests/ui/invalid_pymethods.rs +++ b/tests/ui/invalid_pymethods.rs @@ -45,13 +45,6 @@ impl MyClass { fn setter_without_receiver() {} } -#[pymethods] -impl MyClass { - #[new] - #[pyo3(text_signature = "()")] - fn text_signature_on_new() {} -} - #[pymethods] impl MyClass { #[pyo3(name = "__call__", text_signature = "()")] diff --git a/tests/ui/invalid_pymethods.stderr b/tests/ui/invalid_pymethods.stderr index e660b52c1a3..a4b14baa6eb 100644 --- a/tests/ui/invalid_pymethods.stderr +++ b/tests/ui/invalid_pymethods.stderr @@ -34,115 +34,120 @@ error: expected receiver for #[setter] 45 | fn setter_without_receiver() {} | ^^ -error: `text_signature` not allowed on `__new__`; if you want to add a signature on `__new__`, put it on the struct definition instead - --> tests/ui/invalid_pymethods.rs:51:12 - | -51 | #[pyo3(text_signature = "()")] - | ^^^^^^^^^^^^^^ - error: static method needs #[staticmethod] attribute - --> tests/ui/invalid_pymethods.rs:58:5 + --> tests/ui/invalid_pymethods.rs:51:5 | -58 | fn text_signature_on_call() {} +51 | fn text_signature_on_call() {} | ^^ error: `text_signature` not allowed with `getter` - --> tests/ui/invalid_pymethods.rs:64:12 + --> tests/ui/invalid_pymethods.rs:57:12 | -64 | #[pyo3(text_signature = "()")] +57 | #[pyo3(text_signature = "()")] | ^^^^^^^^^^^^^^ error: `text_signature` not allowed with `setter` - --> tests/ui/invalid_pymethods.rs:71:12 + --> tests/ui/invalid_pymethods.rs:64:12 | -71 | #[pyo3(text_signature = "()")] +64 | #[pyo3(text_signature = "()")] | ^^^^^^^^^^^^^^ error: `text_signature` not allowed with `classattr` - --> tests/ui/invalid_pymethods.rs:78:12 + --> tests/ui/invalid_pymethods.rs:71:12 | -78 | #[pyo3(text_signature = "()")] +71 | #[pyo3(text_signature = "()")] | ^^^^^^^^^^^^^^ error: expected a string literal or `None` - --> tests/ui/invalid_pymethods.rs:84:30 + --> tests/ui/invalid_pymethods.rs:77:30 | -84 | #[pyo3(text_signature = 1)] +77 | #[pyo3(text_signature = 1)] | ^ error: `text_signature` may only be specified once - --> tests/ui/invalid_pymethods.rs:91:12 + --> tests/ui/invalid_pymethods.rs:84:12 | -91 | #[pyo3(text_signature = None)] +84 | #[pyo3(text_signature = None)] | ^^^^^^^^^^^^^^ error: `signature` not allowed with `getter` + --> tests/ui/invalid_pymethods.rs:91:12 + | +91 | #[pyo3(signature = ())] + | ^^^^^^^^^ + +error: `signature` not allowed with `setter` --> tests/ui/invalid_pymethods.rs:98:12 | 98 | #[pyo3(signature = ())] | ^^^^^^^^^ -error: `signature` not allowed with `setter` +error: `signature` not allowed with `classattr` --> tests/ui/invalid_pymethods.rs:105:12 | 105 | #[pyo3(signature = ())] | ^^^^^^^^^ -error: `signature` not allowed with `classattr` - --> tests/ui/invalid_pymethods.rs:112:12 - | -112 | #[pyo3(signature = ())] - | ^^^^^^^^^ - error: cannot specify a second method type - --> tests/ui/invalid_pymethods.rs:119:7 + --> tests/ui/invalid_pymethods.rs:112:7 | -119 | #[staticmethod] +112 | #[staticmethod] | ^^^^^^^^^^^^ error: Python functions cannot have generic type parameters - --> tests/ui/invalid_pymethods.rs:125:23 + --> tests/ui/invalid_pymethods.rs:118:23 | -125 | fn generic_method(value: T) {} +118 | fn generic_method(value: T) {} | ^ error: Python functions cannot have `impl Trait` arguments - --> tests/ui/invalid_pymethods.rs:130:48 + --> tests/ui/invalid_pymethods.rs:123:48 | -130 | fn impl_trait_method_first_arg(impl_trait: impl AsRef) {} +123 | fn impl_trait_method_first_arg(impl_trait: impl AsRef) {} | ^^^^ error: Python functions cannot have `impl Trait` arguments - --> tests/ui/invalid_pymethods.rs:135:56 + --> tests/ui/invalid_pymethods.rs:128:56 | -135 | fn impl_trait_method_second_arg(&self, impl_trait: impl AsRef) {} +128 | fn impl_trait_method_second_arg(&self, impl_trait: impl AsRef) {} | ^^^^ error: `async fn` is not yet supported for Python functions. Additional crates such as `pyo3-asyncio` can be used to integrate async Rust and Python. For more information, see https://github.com/PyO3/pyo3/issues/1632 - --> tests/ui/invalid_pymethods.rs:140:5 + --> tests/ui/invalid_pymethods.rs:133:5 | -140 | async fn async_method(&self) {} +133 | async fn async_method(&self) {} | ^^^^^ error: `pass_module` cannot be used on Python methods - --> tests/ui/invalid_pymethods.rs:145:12 + --> tests/ui/invalid_pymethods.rs:138:12 | -145 | #[pyo3(pass_module)] +138 | #[pyo3(pass_module)] | ^^^^^^^^^^^ error: Python objects are shared, so 'self' cannot be moved out of the Python interpreter. Try `&self`, `&mut self, `slf: PyRef<'_, Self>` or `slf: PyRefMut<'_, Self>`. - --> tests/ui/invalid_pymethods.rs:151:29 + --> tests/ui/invalid_pymethods.rs:144:29 | -151 | fn method_self_by_value(self) {} +144 | fn method_self_by_value(self) {} | ^^^^ +error[E0119]: conflicting implementations of trait `pyo3::impl_::pyclass::PyClassNewTextSignature` for type `pyo3::impl_::pyclass::PyClassImplCollector` + --> tests/ui/invalid_pymethods.rs:149:1 + | +149 | #[pymethods] + | ^^^^^^^^^^^^ + | | + | first implementation here + | conflicting implementation for `pyo3::impl_::pyclass::PyClassImplCollector` + | + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + error[E0592]: duplicate definitions with name `__pymethod___new____` - --> tests/ui/invalid_pymethods.rs:156:1 + --> tests/ui/invalid_pymethods.rs:149:1 | -156 | #[pymethods] +149 | #[pymethods] | ^^^^^^^^^^^^ | | | duplicate definitions for `__pymethod___new____` @@ -151,9 +156,9 @@ error[E0592]: duplicate definitions with name `__pymethod___new____` = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0592]: duplicate definitions with name `__pymethod_func__` - --> tests/ui/invalid_pymethods.rs:171:1 + --> tests/ui/invalid_pymethods.rs:164:1 | -171 | #[pymethods] +164 | #[pymethods] | ^^^^^^^^^^^^ | | | duplicate definitions for `__pymethod_func__`