From 31864ec0bebcae6a23f326f12bc93fa191bb12be Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Sun, 17 Oct 2021 22:49:10 +0100 Subject: [PATCH] pymethods: support __call__ proto --- CHANGELOG.md | 1 + benches/bench_pyclass.rs | 3 +- guide/src/class.md | 76 +-------------------- guide/src/class/protocols.md | 89 +++++++++++++++++++++---- pyo3-macros-backend/src/deprecations.rs | 2 + pyo3-macros-backend/src/method.rs | 61 ++++++++--------- pyo3-macros-backend/src/pyclass.rs | 5 -- pyo3-macros-backend/src/pymethod.rs | 26 ++++---- pyo3-macros/src/lib.rs | 1 - src/class/impl_.rs | 13 ---- src/impl_/deprecations.rs | 3 + src/pyclass.rs | 4 -- tests/hygiene/pymethods.rs | 1 - tests/test_methods.rs | 3 +- tests/test_multiple_pymethods.rs | 3 +- tests/test_proto_methods.rs | 35 ++++++++-- tests/ui/deprecations.rs | 3 + tests/ui/deprecations.stderr | 34 ++++++---- tests/ui/invalid_pymethods.rs | 12 ++-- tests/ui/invalid_pymethods.stderr | 6 -- tests/ui/pyclass_send.stderr | 4 +- 21 files changed, 191 insertions(+), 194 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43aa84578a1..2333d2d9204 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Move Py_DecodeLocale from sysmodule to fileutils. [#1887](https://github.com/PyO3/pyo3/pull/1887) - Deprecate `PySys_AddWarnOption`, `PySys_AddWarnOptionUnicode` and `PySys_HasWarnOptions`. [#1887](https://github.com/PyO3/pyo3/pull/1887) - Remove function PyTuple_ClearFreeList from python 3.9 above. [#1887](https://github.com/PyO3/pyo3/pull/1887) +- Deprecate `#[call]` attribute in favor of using `fn __call__`. [#1929](https://github.com/PyO3/pyo3/pull/1929) ### Fixed diff --git a/benches/bench_pyclass.rs b/benches/bench_pyclass.rs index 1928a5ce6d5..4cfe737bd42 100644 --- a/benches/bench_pyclass.rs +++ b/benches/bench_pyclass.rs @@ -16,8 +16,7 @@ impl MyClass { Self { elements } } - #[call] - fn call(&mut self, new_element: i32) -> usize { + fn __call__(&mut self, new_element: i32) -> usize { self.elements.push(new_element); self.elements.len() } diff --git a/guide/src/class.md b/guide/src/class.md index 3551ad20a30..7c21c011e7b 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -14,7 +14,6 @@ This chapter will discuss the functionality and configuration these attributes o - [`#[setter]`](#object-properties-using-getter-and-setter) - [`#[staticmethod]`](#static-methods) - [`#[classmethod]`](#class-methods) - - [`#[call]`](#callable-objects) - [`#[classattr]`](#class-attributes) - [`#[args]`](#method-arguments) - [`#[pyproto]`](class/protocols.html) @@ -46,7 +45,7 @@ Custom Python classes can then be added to a module using `add_class()`. # use pyo3::prelude::*; # #[pyclass] # struct MyClass { -# #[allow(dead_code)] +# #[allow(dead_code)] # num: i32, # } #[pymodule] @@ -613,74 +612,6 @@ impl MyClass { } ``` -## Callable objects - -To specify a custom `__call__` method for a custom class, the method needs to be annotated with -the `#[call]` attribute. Arguments of the method are specified as for instance methods. - -The following pyclass is a basic decorator - its constructor takes a Python object -as argument and calls that object when called. - -```rust -# use pyo3::prelude::*; -# use pyo3::types::{PyDict, PyTuple}; -# -#[pyclass(name = "counter")] -struct PyCounter { - count: u64, - wraps: Py, -} - -#[pymethods] -impl PyCounter { - #[new] - fn __new__(wraps: Py) -> Self { - PyCounter { count: 0, wraps } - } - - #[call] - #[args(args = "*", kwargs = "**")] - fn __call__( - &mut self, - py: Python, - args: &PyTuple, - kwargs: Option<&PyDict>, - ) -> PyResult> { - self.count += 1; - let name = self.wraps.getattr(py, "__name__").unwrap(); - - println!("{} has been called {} time(s).", name, self.count); - self.wraps.call(py, args, kwargs) - } -} -``` - -Python code: - -```python -@counter -def say_hello(): - print("hello") - -say_hello() -say_hello() -say_hello() -say_hello() -``` - -Output: - -```text -say_hello has been called 1 time(s). -hello -say_hello has been called 2 time(s). -hello -say_hello has been called 3 time(s). -hello -say_hello has been called 4 time(s). -hello -``` - ## Method arguments By default, PyO3 uses function signatures to determine which arguments are required. Then it scans @@ -845,11 +776,6 @@ impl pyo3::class::impl_::PyClassImpl for MyClass { let collector = PyClassImplCollector::::new(); collector.free_impl() } - fn get_call() -> Option { - use pyo3::class::impl_::*; - let collector = PyClassImplCollector::::new(); - collector.call_impl() - } fn for_each_proto_slot(visitor: &mut dyn FnMut(&[pyo3::ffi::PyType_Slot])) { // Implementation which uses dtolnay specialization to load all slots. use pyo3::class::impl_::*; diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index 5ace236c864..5217ba2df90 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -1,4 +1,4 @@ -## Class customizations +# Class customizations PyO3 uses the `#[pyproto]` attribute in combination with special traits to implement certain protocol (aka `__dunder__`) methods of Python classes. The special traits are listed in this chapter of the guide. See also the [documentation for the `pyo3::class` module]({{#PYO3_DOCS_URL}}/pyo3/class/index.html). @@ -10,11 +10,11 @@ All `#[pyproto]` methods can return `T` instead of `PyResult` if the method i There are many "dunder" methods which are not included in any of PyO3's protocol traits, such as `__dir__`. These methods can be implemented in `#[pymethods]` as already covered in the previous section. -### Basic object customization +## Basic object customization The [`PyObjectProtocol`] trait provides several basic customizations. -#### Attribute access +### Attribute access To customize object attribute access, define the following methods: @@ -24,14 +24,14 @@ To customize object attribute access, define the following methods: Each method corresponds to Python's `self.attr`, `self.attr = value` and `del self.attr` code. -#### String Conversions +### String Conversions * `fn __repr__(&self) -> PyResult>` * `fn __str__(&self) -> PyResult>` Possible return types for `__str__` and `__repr__` are `PyResult` or `PyResult`. -#### Comparison operators +### Comparison operators * `fn __richcmp__(&self, other: impl FromPyObject, op: CompareOp) -> PyResult` @@ -46,13 +46,78 @@ Each method corresponds to Python's `self.attr`, `self.attr = value` and `del se Objects that compare equal must have the same hash value. The return type must be `PyResult` where `T` is one of Rust's primitive integer types. -#### Other methods +### Other methods * `fn __bool__(&self) -> PyResult` Determines the "truthyness" of the object. -### Emulating numeric types +## Callable objects + +Custom classes can be callable if they have a `#[pymethod]` named `__call__`. + +The following pyclass is a basic decorator - its constructor takes a Python object +as argument and calls that object when called. + +```rust +# use pyo3::prelude::*; +# use pyo3::types::{PyDict, PyTuple}; +# +#[pyclass(name = "counter")] +struct PyCounter { + count: u64, + wraps: Py, +} + +#[pymethods] +impl PyCounter { + #[new] + fn __new__(wraps: Py) -> Self { + PyCounter { count: 0, wraps } + } + + fn __call__( + &mut self, + py: Python, + args: &PyTuple, + kwargs: Option<&PyDict>, + ) -> PyResult> { + self.count += 1; + let name = self.wraps.getattr(py, "__name__").unwrap(); + + println!("{} has been called {} time(s).", name, self.count); + self.wraps.call(py, args, kwargs) + } +} +``` + +Python code: + +```python +@counter +def say_hello(): + print("hello") + +say_hello() +say_hello() +say_hello() +say_hello() +``` + +Output: + +```text +say_hello has been called 1 time(s). +hello +say_hello has been called 2 time(s). +hello +say_hello has been called 3 time(s). +hello +say_hello has been called 4 time(s). +hello +``` + +## Emulating numeric types The [`PyNumberProtocol`] trait can be implemented to emulate [numeric types](https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types). @@ -132,7 +197,7 @@ Other: * `fn __index__(&'p self) -> PyResult` -### Emulating sequential containers (such as lists or tuples) +## Emulating sequential containers (such as lists or tuples) The [`PySequenceProtocol`] trait can be implemented to emulate [sequential container types](https://docs.python.org/3/reference/datamodel.html#emulating-container-types). @@ -195,7 +260,7 @@ where _N_ is the length of the sequence. Used by the `*=` operator, after trying the numeric in place multiplication via the `PyNumberProtocol` trait method. -### Emulating mapping containers (such as dictionaries) +## Emulating mapping containers (such as dictionaries) The [`PyMappingProtocol`] trait allows to emulate [mapping container types](https://docs.python.org/3/reference/datamodel.html#emulating-container-types). @@ -228,7 +293,7 @@ For a mapping, the keys may be Python objects of arbitrary type. The same exceptions should be raised for improper key values as for the `__getitem__()` method. -### Garbage Collector Integration +## Garbage Collector Integration If your type owns references to other Python objects, you will need to integrate with Python's garbage collector so that the GC is aware of @@ -280,7 +345,7 @@ at compile time: struct GCTracked {} // Fails because it does not implement PyGCProtocol ``` -### Iterator Types +## Iterator Types Iterators can be defined using the [`PyIterProtocol`]({{#PYO3_DOCS_URL}}/pyo3/class/iter/trait.PyIterProtocol.html) trait. @@ -365,7 +430,7 @@ impl PyIterProtocol for Container { For more details on Python's iteration protocols, check out [the "Iterator Types" section of the library documentation](https://docs.python.org/3/library/stdtypes.html#iterator-types). -#### Returning a value from iteration +### Returning a value from iteration This guide has so far shown how to use `Option` to implement yielding values during iteration. In Python a generator can also return a value. To express this in Rust, PyO3 provides the diff --git a/pyo3-macros-backend/src/deprecations.rs b/pyo3-macros-backend/src/deprecations.rs index 0c4e28b9b5f..b178d4e8989 100644 --- a/pyo3-macros-backend/src/deprecations.rs +++ b/pyo3-macros-backend/src/deprecations.rs @@ -6,6 +6,7 @@ pub enum Deprecation { PyfnNameArgument, PyModuleNameArgument, TextSignatureAttribute, + CallAttribute, } impl Deprecation { @@ -15,6 +16,7 @@ impl Deprecation { Deprecation::PyfnNameArgument => "PYFN_NAME_ARGUMENT", Deprecation::PyModuleNameArgument => "PYMODULE_NAME_ARGUMENT", Deprecation::TextSignatureAttribute => "TEXT_SIGNATURE_ATTRIBUTE", + Deprecation::CallAttribute => "CALL_ATTRIBUTE", }; syn::Ident::new(string, span) } diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index e05d67e41ea..f70b70790f7 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -1,6 +1,7 @@ // Copyright (c) 2017-present PyO3 Project and Contributors use crate::attributes::TextSignatureAttribute; +use crate::deprecations::Deprecation; use crate::params::{accept_args_kwargs, impl_arg_params}; use crate::pyfunction::PyFunctionOptions; use crate::pyfunction::{PyFunctionArgPyO3Attributes, PyFunctionSignature}; @@ -65,8 +66,6 @@ impl<'a> FnArg<'a> { pub enum MethodTypeAttribute { /// #[new] New, - /// #[call] - Call, /// #[classmethod] ClassMethod, /// #[classattr] @@ -84,7 +83,6 @@ pub enum FnType { Getter(SelfType), Setter(SelfType), Fn(SelfType), - FnCall(SelfType), FnNew, FnClass, FnStatic, @@ -99,11 +97,10 @@ impl FnType { error_mode: ExtractErrorMode, ) -> TokenStream { match self { - FnType::Getter(st) | FnType::Setter(st) | FnType::Fn(st) | FnType::FnCall(st) => st - .receiver( - cls.expect("no class given for Fn with a \"self\" receiver"), - error_mode, - ), + FnType::Getter(st) | FnType::Setter(st) | FnType::Fn(st) => st.receiver( + cls.expect("no class given for Fn with a \"self\" receiver"), + error_mode, + ), FnType::FnNew | FnType::FnStatic | FnType::ClassAttribute => { quote!() } @@ -260,11 +257,18 @@ impl<'a> FnSpec<'a> { meth_attrs: &mut Vec, options: PyFunctionOptions, ) -> Result> { + let PyFunctionOptions { + text_signature, + name, + mut deprecations, + .. + } = options; + let MethodAttributes { ty: fn_type_attr, args: fn_attrs, mut python_name, - } = parse_method_attributes(meth_attrs, options.name.map(|name| name.0))?; + } = parse_method_attributes(meth_attrs, name.map(|name| name.0), &mut deprecations)?; match fn_type_attr { Some(MethodTypeAttribute::New) => { @@ -273,18 +277,12 @@ impl<'a> FnSpec<'a> { } python_name = Some(syn::Ident::new("__new__", Span::call_site())) } - Some(MethodTypeAttribute::Call) => { - if let Some(name) = &python_name { - bail_spanned!(name.span() => "`name` not allowed with `#[call]`"); - } - python_name = Some(syn::Ident::new("__call__", Span::call_site())) - } _ => {} } let (fn_type, skip_first_arg, fixed_convention) = Self::parse_fn_type(sig, fn_type_attr, &mut python_name)?; - Self::ensure_text_signature_on_valid_method(&fn_type, options.text_signature.as_ref())?; + Self::ensure_text_signature_on_valid_method(&fn_type, text_signature.as_ref())?; let name = &sig.ident; let ty = get_return_info(&sig.output); @@ -292,10 +290,7 @@ impl<'a> FnSpec<'a> { let doc = utils::get_doc( meth_attrs, - options - .text_signature - .as_ref() - .map(|attr| (&python_name, attr)), + text_signature.as_ref().map(|attr| (&python_name, attr)), ); let arguments: Vec<_> = if skip_first_arg { @@ -323,7 +318,7 @@ impl<'a> FnSpec<'a> { args: arguments, output: ty, doc, - deprecations: options.deprecations, + deprecations, }) } @@ -342,10 +337,7 @@ impl<'a> FnSpec<'a> { "text_signature not allowed on __new__; if you want to add a signature on \ __new__, put it on the struct definition instead" ), - FnType::FnCall(_) - | FnType::Getter(_) - | FnType::Setter(_) - | FnType::ClassAttribute => bail_spanned!( + FnType::Getter(_) | FnType::Setter(_) | FnType::ClassAttribute => bail_spanned!( text_signature.kw.span() => "text_signature not allowed with this method type" ), _ => {} @@ -392,11 +384,6 @@ impl<'a> FnSpec<'a> { (FnType::FnNew, false, Some(CallingConvention::TpNew)) } Some(MethodTypeAttribute::ClassMethod) => (FnType::FnClass, true, None), - Some(MethodTypeAttribute::Call) => ( - FnType::FnCall(parse_receiver("expected receiver for #[call]")?), - true, - Some(CallingConvention::Varargs), - ), Some(MethodTypeAttribute::Getter) => { // Strip off "get_" prefix if needed if python_name.is_none() { @@ -611,6 +598,7 @@ struct MethodAttributes { fn parse_method_attributes( attrs: &mut Vec, mut python_name: Option, + deprecations: &mut Deprecations, ) -> Result { let mut new_attrs = Vec::new(); let mut args = Vec::new(); @@ -633,7 +621,12 @@ fn parse_method_attributes( } else if name.is_ident("init") || name.is_ident("__init__") { bail_spanned!(name.span() => "#[init] is disabled since PyO3 0.9.0"); } else if name.is_ident("call") || name.is_ident("__call__") { - set_ty!(MethodTypeAttribute::Call, name); + deprecations.push(Deprecation::CallAttribute, name.span()); + ensure_spanned!( + python_name.is_none(), + python_name.span() => "`name` may not be used with `#[call]`" + ); + python_name = Some(syn::Ident::new("__call__", Span::call_site())); } else if name.is_ident("classmethod") { set_ty!(MethodTypeAttribute::ClassMethod, name); } else if name.is_ident("staticmethod") { @@ -663,7 +656,11 @@ fn parse_method_attributes( } else if path.is_ident("init") { bail_spanned!(path.span() => "#[init] is disabled since PyO3 0.9.0"); } else if path.is_ident("call") { - set_ty!(MethodTypeAttribute::Call, path); + ensure_spanned!( + python_name.is_none(), + python_name.span() => "`name` may not be used with `#[call]`" + ); + python_name = Some(syn::Ident::new("__call__", Span::call_site())); } else if path.is_ident("setter") || path.is_ident("getter") { if let syn::AttrStyle::Inner(_) = attr.style { bail_spanned!( diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index c68a67c4e23..d747040dc66 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -579,11 +579,6 @@ fn impl_class( let collector = PyClassImplCollector::::new(); collector.free_impl() } - fn get_call() -> ::std::option::Option<::pyo3::ffi::PyCFunctionWithKeywords> { - use ::pyo3::class::impl_::*; - let collector = PyClassImplCollector::::new(); - collector.call_impl() - } fn for_each_proto_slot(visitor: &mut dyn ::std::ops::FnMut(&[::pyo3::ffi::PyType_Slot])) { // Implementation which uses dtolnay specialization to load all slots. diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index c8c451f0e8e..dfcac58761c 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use crate::attributes::NameAttribute; -use crate::method::ExtractErrorMode; +use crate::method::{CallingConvention, ExtractErrorMode}; use crate::utils::{ensure_not_async_fn, unwrap_ty_group, PythonDoc}; use crate::{deprecations::Deprecations, utils}; use crate::{ @@ -38,6 +38,8 @@ pub fn gen_py_method( if let Some(slot_def) = pyproto(&method_name) { let slot = slot_def.generate_type_slot(cls, &spec)?; return Ok(GeneratedPyMethod::Proto(slot)); + } else if method_name == "__call__" { + return Ok(GeneratedPyMethod::Proto(impl_call_slot(cls, spec)?)); } if let Some(slot_fragment_def) = pyproto_fragment(&method_name) { @@ -60,7 +62,6 @@ pub fn gen_py_method( )?), // special prototypes FnType::FnNew => GeneratedPyMethod::TraitImpl(impl_py_method_def_new(cls, &spec)?), - FnType::FnCall(_) => GeneratedPyMethod::TraitImpl(impl_py_method_def_call(cls, &spec)?), FnType::ClassAttribute => GeneratedPyMethod::Method(impl_py_class_attribute(cls, &spec)), FnType::Getter(self_type) => GeneratedPyMethod::Method(impl_py_getter_def( cls, @@ -136,19 +137,20 @@ fn impl_py_method_def_new(cls: &syn::Type, spec: &FnSpec) -> Result }) } -fn impl_py_method_def_call(cls: &syn::Type, spec: &FnSpec) -> Result { +fn impl_call_slot(cls: &syn::Type, mut spec: FnSpec) -> Result { + // HACK: __call__ proto slot must always use varargs calling convention, so change the spec. + // Probably indicates there's a refactoring opportunity somewhere. + spec.convention = CallingConvention::Varargs; + let wrapper_ident = syn::Ident::new("__wrap", Span::call_site()); let wrapper = spec.get_wrapper_function(&wrapper_ident, Some(cls))?; - Ok(quote! { - impl ::pyo3::class::impl_::PyClassCallImpl<#cls> for ::pyo3::class::impl_::PyClassImplCollector<#cls> { - fn call_impl(self) -> ::std::option::Option<::pyo3::ffi::PyCFunctionWithKeywords> { - ::std::option::Option::Some({ - #wrapper - #wrapper_ident - }) - } + Ok(quote! {{ + #wrapper + ::pyo3::ffi::PyType_Slot { + slot: ::pyo3::ffi::Py_tp_call, + pfunc: __wrap as ::pyo3::ffi::ternaryfunc as _ } - }) + }}) } fn impl_py_class_attribute(cls: &syn::Type, spec: &FnSpec) -> TokenStream { diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index bd72cd3c9e2..d91553995cc 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -133,7 +133,6 @@ pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream { /// | [`#[getter]`][5] and [`#[setter]`][5] | These define getters and setters, similar to Python's `@property` decorator. This is useful for getters/setters that require computation or side effects; if that is not the case consider using [`#[pyo3(get, set)]`][11] on the struct's field(s).| /// | [`#[staticmethod]`][6]| Defines the method as a staticmethod, like Python's `@staticmethod` decorator.| /// | [`#[classmethod]`][7] | Defines the method as a classmethod, like Python's `@classmethod` decorator.| -/// | [`#[call]`][8] | Allows Python code to call a class instance as a function, like Python's `__call__` method. | /// | [`#[classattr]`][9] | Defines a class variable. | /// | [`#[args]`][10] | Define a method's default arguments and allows the function to receive `*args` and `**kwargs`. | /// diff --git a/src/class/impl_.rs b/src/class/impl_.rs index 125e24a3572..0102723997c 100644 --- a/src/class/impl_.rs +++ b/src/class/impl_.rs @@ -71,9 +71,6 @@ pub trait PyClassImpl: Sized { fn get_new() -> Option { None } - fn get_call() -> Option { - None - } fn get_alloc() -> Option { None } @@ -98,16 +95,6 @@ impl PyClassNewImpl for &'_ PyClassImplCollector { } } -pub trait PyClassCallImpl { - fn call_impl(self) -> Option; -} - -impl PyClassCallImpl for &'_ PyClassImplCollector { - fn call_impl(self) -> Option { - None - } -} - macro_rules! slot_fragment_trait { ($trait_name:ident, $($default_method:tt)*) => { #[allow(non_camel_case_types)] diff --git a/src/impl_/deprecations.rs b/src/impl_/deprecations.rs index 585b59dc88e..1fbabf9eefd 100644 --- a/src/impl_/deprecations.rs +++ b/src/impl_/deprecations.rs @@ -23,3 +23,6 @@ pub const PYMODULE_NAME_ARGUMENT: () = (); note = "use `#[pyo3(text_signature = \"...\")]` instead of `#[text_signature = \"...\"]`" )] pub const TEXT_SIGNATURE_ATTRIBUTE: () = (); + +#[deprecated(since = "0.15.0", note = "use `fn __call__` instead of `#[call]`")] +pub const CALL_ATTRIBUTE: () = (); diff --git a/src/pyclass.rs b/src/pyclass.rs index 47c9d6f037e..61412a6197f 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -83,10 +83,6 @@ where slots.push(ffi::Py_tp_free, free as _); } - if let Some(call_meth) = T::get_call() { - slots.push(ffi::Py_tp_call, call_meth as _); - } - if cfg!(Py_3_9) { let members = py_class_members::(); if !members.is_empty() { diff --git a/tests/hygiene/pymethods.rs b/tests/hygiene/pymethods.rs index fc2e3547262..37a916f9c6b 100644 --- a/tests/hygiene/pymethods.rs +++ b/tests/hygiene/pymethods.rs @@ -363,7 +363,6 @@ impl Dummy { fn staticmethod() {} #[classmethod] fn clsmethod(_: &::pyo3::types::PyType) {} - #[call] #[args(args = "*", kwds = "**")] fn __call__( &self, diff --git a/tests/test_methods.rs b/tests/test_methods.rs index f238278091b..f161ccba044 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -669,8 +669,7 @@ impl r#RawIdents { self.r#subsubtype = r#subsubtype; } - #[call] - pub fn r#call(&mut self, r#type: PyObject) { + pub fn r#__call__(&mut self, r#type: PyObject) { self.r#type = r#type; } diff --git a/tests/test_multiple_pymethods.rs b/tests/test_multiple_pymethods.rs index ab249ea9eba..a793eec5e32 100644 --- a/tests/test_multiple_pymethods.rs +++ b/tests/test_multiple_pymethods.rs @@ -20,8 +20,7 @@ impl PyClassWithMultiplePyMethods { #[pymethods] impl PyClassWithMultiplePyMethods { - #[call] - fn call(&self) -> &'static str { + fn __call__(&self) -> &'static str { "call" } } diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index 1cfafefdedb..f0cada77e25 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -229,32 +229,57 @@ fn iterator() { } #[pyclass] -struct Callable {} +struct Callable; #[pymethods] impl Callable { - #[__call__] fn __call__(&self, arg: i32) -> i32 { arg * 6 } } #[pyclass] -struct EmptyClass; +struct NotCallable; #[test] fn callable() { let gil = Python::acquire_gil(); let py = gil.python(); - let c = Py::new(py, Callable {}).unwrap(); + let c = Py::new(py, Callable).unwrap(); py_assert!(py, c, "callable(c)"); py_assert!(py, c, "c(7) == 42"); - let nc = Py::new(py, EmptyClass).unwrap(); + let nc = Py::new(py, NotCallable).unwrap(); py_assert!(py, nc, "not callable(nc)"); } +#[allow(deprecated)] +mod deprecated { + use super::*; + + #[pyclass] + struct Callable; + + #[pymethods] + impl Callable { + #[__call__] + fn __call__(&self, arg: i32) -> i32 { + arg * 6 + } + } + + #[test] + fn callable() { + let gil = Python::acquire_gil(); + let py = gil.python(); + + let c = Py::new(py, Callable).unwrap(); + py_assert!(py, c, "callable(c)"); + py_assert!(py, c, "c(7) == 42"); + } +} + #[pyclass] #[derive(Debug)] struct SetItem { diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index ca4805a32bb..83a91dc5478 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -22,6 +22,9 @@ impl TestClass { #[name = "custom_static"] #[text_signature = "()"] fn deprecated_name_staticmethod() {} + + #[call] + fn deprecated_call(&self) {} } #[pyfunction] diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index 0b78dcaf0d8..7367731627f 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -1,8 +1,8 @@ -error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` - --> tests/ui/deprecations.rs:14:5 +error: use of deprecated constant `pyo3::impl_::deprecations::CALL_ATTRIBUTE`: use `fn __call__` instead of `#[call]` + --> tests/ui/deprecations.rs:26:7 | -14 | #[name = "num"] - | ^ +26 | #[call] + | ^^^^ | note: the lint level is defined here --> tests/ui/deprecations.rs:1:9 @@ -10,6 +10,12 @@ note: the lint level is defined here 1 | #![deny(deprecated)] | ^^^^^^^^^^ +error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` + --> tests/ui/deprecations.rs:14:5 + | +14 | #[name = "num"] + | ^ + error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` --> tests/ui/deprecations.rs:17:5 | @@ -35,33 +41,33 @@ error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATT | ^ error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]` - --> tests/ui/deprecations.rs:28:1 + --> tests/ui/deprecations.rs:31:1 | -28 | #[name = "foo"] +31 | #[name = "foo"] | ^ error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` - --> tests/ui/deprecations.rs:29:1 + --> tests/ui/deprecations.rs:32:1 | -29 | #[text_signature = "()"] +32 | #[text_signature = "()"] | ^ error: use of deprecated constant `pyo3::impl_::deprecations::PYFN_NAME_ARGUMENT`: use `#[pyfn(m)] #[pyo3(name = "...")]` instead of `#[pyfn(m, "...")]` - --> tests/ui/deprecations.rs:34:15 + --> tests/ui/deprecations.rs:37:15 | -34 | #[pyfn(m, "some_name")] +37 | #[pyfn(m, "some_name")] | ^^^^^^^^^^^ error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` - --> tests/ui/deprecations.rs:35:5 + --> tests/ui/deprecations.rs:38:5 | -35 | #[text_signature = "()"] +38 | #[text_signature = "()"] | ^ error: use of deprecated constant `pyo3::impl_::deprecations::PYMODULE_NAME_ARGUMENT`: use `#[pymodule] #[pyo3(name = "...")]` instead of `#[pymodule(...)]` - --> tests/ui/deprecations.rs:32:12 + --> tests/ui/deprecations.rs:35:12 | -32 | #[pymodule(deprecated_module_name)] +35 | #[pymodule(deprecated_module_name)] | ^^^^^^^^^^^^^^^^^^^^^^ error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]` diff --git a/tests/ui/invalid_pymethods.rs b/tests/ui/invalid_pymethods.rs index 528af3197c1..fa9ee2bafd8 100644 --- a/tests/ui/invalid_pymethods.rs +++ b/tests/ui/invalid_pymethods.rs @@ -52,12 +52,12 @@ impl MyClass { fn text_signature_on_new() {} } -#[pymethods] -impl MyClass { - #[call] - #[pyo3(text_signature = "()")] - fn text_signature_on_call(&self) {} -} +// FIXME: this doesn't fail - should refuse text signature on protocol methods in general? +// #[pymethods] +// impl MyClass { +// #[pyo3(text_signature = "()")] +// fn __call__(&self) {} +// } #[pymethods] impl MyClass { diff --git a/tests/ui/invalid_pymethods.stderr b/tests/ui/invalid_pymethods.stderr index 6265c080ecc..4621a4492dc 100644 --- a/tests/ui/invalid_pymethods.stderr +++ b/tests/ui/invalid_pymethods.stderr @@ -40,12 +40,6 @@ error: text_signature not allowed on __new__; if you want to add a signature on 51 | #[pyo3(text_signature = "()")] | ^^^^^^^^^^^^^^ -error: text_signature not allowed with this method type - --> tests/ui/invalid_pymethods.rs:58:12 - | -58 | #[pyo3(text_signature = "()")] - | ^^^^^^^^^^^^^^ - error: text_signature not allowed with this method type --> tests/ui/invalid_pymethods.rs:65:12 | diff --git a/tests/ui/pyclass_send.stderr b/tests/ui/pyclass_send.stderr index a4228c12e24..779bd0ddcf7 100644 --- a/tests/ui/pyclass_send.stderr +++ b/tests/ui/pyclass_send.stderr @@ -4,9 +4,9 @@ error[E0277]: `Rc` cannot be sent between threads safely 4 | #[pyclass] | ^^^^^^^^^^ `Rc` cannot be sent between threads safely | - ::: src/class/impl_.rs:719:33 + ::: src/class/impl_.rs:706:33 | -719 | pub struct ThreadCheckerStub(PhantomData); +706 | pub struct ThreadCheckerStub(PhantomData); | ---- required by this bound in `ThreadCheckerStub` | = help: within `NotThreadSafe`, the trait `Send` is not implemented for `Rc`