support `text_signature` on `#[new]`

This commit is contained in:
David Hewitt 2023-02-07 21:15:32 +00:00
parent 157e0b4ad6
commit 8bd17f02c7
20 changed files with 410 additions and 276 deletions

View File

@ -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<PyObject> 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<MyClass>;
@ -1041,6 +1039,15 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass {
static TYPE_OBJECT: LazyTypeObject<MyClass> = 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::<Self>::new();
build_pyclass_doc(<MyClass as pyo3::PyTypeInfo>::NAME, "", None.or_else(|| collector.new_text_signature()))
}).map(::std::ops::Deref::deref)
}
}
# Python::with_gil(|py| {

View File

@ -42,7 +42,7 @@ fn wrap(obj: &PyAny) -> Result<i32, PyErr> {
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<i32, PyErr> {
/// 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<i32, PyErr> {
/// 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

View File

@ -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.)

View File

@ -0,0 +1 @@
Support `text_signature` option (and automatically generate signature) for `#[new]` in `#[pymethods]`.

View File

@ -0,0 +1 @@
Deprecate `text_signature` option on `#[pyclass]`.

View File

@ -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",

View File

@ -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<String> {
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`")
}

View File

@ -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<TokenStream> {
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<TokenStream> {
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<TokenStream> {
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<TokenStream> {
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::<Self>::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

View File

@ -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<String> {
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<String> {
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,
}
}

View File

@ -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('(');

View File

@ -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<MethodAndSlotDef> {
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<MethodAn
kwargs: *mut _pyo3::ffi::PyObject,
) -> *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,

View File

@ -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::<TokenStream, Token![,]>::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<String>) -> 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::<TokenStream, Token![,]>::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") {

View File

@ -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: () = ();

View File

@ -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<ffi::Py_ssize_t> {
None
}
#[inline]
fn weaklist_offset() -> Option<ffi::Py_ssize_t> {
None
@ -198,6 +202,29 @@ pub trait PyClassImpl: Sized + 'static {
fn lazy_type_object() -> &'static LazyTypeObject<Self>;
}
/// 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<Cow<'static, CStr>> {
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<T> PyMethods<T> for &'_ PyClassImplCollector<T> {
}
}
// Text signature for __new__
pub trait PyClassNewTextSignature<T> {
fn new_text_signature(self) -> Option<&'static str>;
}
impl<T> PyClassNewTextSignature<T> for &'_ PyClassImplCollector<T> {
#[inline]
fn new_text_signature(self) -> Option<&'static str> {
None
}
}
// Thread checkers
#[doc(hidden)]

View File

@ -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<Cow<'static, CStr>> {
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)
}

View File

@ -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<Cow<'static, CStr>> {
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)
}

View File

@ -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::<T> 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) }
}
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)))]
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 _;
}))
// 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!(
"{}.{}",

View File

@ -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<i32>, 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::<MyClass>();
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<i32>, 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::<MyClass>();
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::<MyClass>();
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::<MyClass>();
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::<MyClass>();
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<i32>, c: i32) -> Self {
let _ = (a, b, c);
Self {}
}
}
Python::with_gil(|py| {
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]
#[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<i32>, c: i32) -> Self {
let _ = (a, b, c);
Self {}
}
}
Python::with_gil(|py| {
let typeobj = py.get_type::<MyClass>();
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<i32>, c: i32) -> Self {
let _ = (a, b, c);
Self {}
}
}
Python::with_gil(|py| {
let typeobj = py.get_type::<MyClass>();
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)'"
);
});
}
}

View File

@ -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 = "()")]

View File

@ -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:57:12
|
57 | #[pyo3(text_signature = "()")]
| ^^^^^^^^^^^^^^
error: `text_signature` not allowed with `setter`
--> tests/ui/invalid_pymethods.rs:64:12
|
64 | #[pyo3(text_signature = "()")]
| ^^^^^^^^^^^^^^
error: `text_signature` not allowed with `setter`
error: `text_signature` not allowed with `classattr`
--> tests/ui/invalid_pymethods.rs:71:12
|
71 | #[pyo3(text_signature = "()")]
| ^^^^^^^^^^^^^^
error: `text_signature` not allowed with `classattr`
--> tests/ui/invalid_pymethods.rs:78:12
|
78 | #[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<T>(value: T) {}
118 | fn generic_method<T>(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<PyAny>) {}
123 | fn impl_trait_method_first_arg(impl_trait: impl AsRef<PyAny>) {}
| ^^^^
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<PyAny>) {}
128 | fn impl_trait_method_second_arg(&self, impl_trait: impl AsRef<PyAny>) {}
| ^^^^
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[E0592]: duplicate definitions with name `__pymethod___new____`
--> tests/ui/invalid_pymethods.rs:156:1
error[E0119]: conflicting implementations of trait `pyo3::impl_::pyclass::PyClassNewTextSignature<TwoNew>` for type `pyo3::impl_::pyclass::PyClassImplCollector<TwoNew>`
--> tests/ui/invalid_pymethods.rs:149:1
|
156 | #[pymethods]
149 | #[pymethods]
| ^^^^^^^^^^^^
| |
| first implementation here
| conflicting implementation for `pyo3::impl_::pyclass::PyClassImplCollector<TwoNew>`
|
= 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:149:1
|
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__`