Skip to content

Commit

Permalink
pymethods: support __call__ proto
Browse files Browse the repository at this point in the history
  • Loading branch information
davidhewitt committed Oct 20, 2021
1 parent 7349513 commit 801b629
Show file tree
Hide file tree
Showing 21 changed files with 201 additions and 204 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,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

Expand Down
3 changes: 1 addition & 2 deletions benches/bench_pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
76 changes: 1 addition & 75 deletions guide/src/class.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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<PyAny>,
}

#[pymethods]
impl PyCounter {
#[new]
fn __new__(wraps: Py<PyAny>) -> Self {
PyCounter { count: 0, wraps }
}

#[call]
#[args(args = "*", kwargs = "**")]
fn __call__(
&mut self,
py: Python,
args: &PyTuple,
kwargs: Option<&PyDict>,
) -> PyResult<Py<PyAny>> {
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
Expand Down Expand Up @@ -848,11 +779,6 @@ impl pyo3::class::impl_::PyClassImpl for MyClass {
let collector = PyClassImplCollector::<Self>::new();
collector.free_impl()
}
fn get_call() -> Option<pyo3::ffi::PyCFunctionWithKeywords> {
use pyo3::class::impl_::*;
let collector = PyClassImplCollector::<Self>::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_::*;
Expand Down
89 changes: 77 additions & 12 deletions guide/src/class/protocols.md
Original file line number Diff line number Diff line change
@@ -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).

Expand All @@ -10,11 +10,11 @@ All `#[pyproto]` methods can return `T` instead of `PyResult<T>` 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:

Expand All @@ -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<impl ToPyObject<ObjectType=PyString>>`
* `fn __str__(&self) -> PyResult<impl ToPyObject<ObjectType=PyString>>`

Possible return types for `__str__` and `__repr__` are `PyResult<String>` or `PyResult<PyString>`.

#### Comparison operators
### Comparison operators

* `fn __richcmp__(&self, other: impl FromPyObject, op: CompareOp) -> PyResult<impl ToPyObject>`

Expand All @@ -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<T>` where `T` is one of Rust's primitive integer types.

#### Other methods
### Other methods

* `fn __bool__(&self) -> PyResult<bool>`

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<PyAny>,
}

#[pymethods]
impl PyCounter {
#[new]
fn __new__(wraps: Py<PyAny>) -> Self {
PyCounter { count: 0, wraps }
}

fn __call__(
&mut self,
py: Python,
args: &PyTuple,
kwargs: Option<&PyDict>,
) -> PyResult<Py<PyAny>> {
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).

Expand Down Expand Up @@ -132,7 +197,7 @@ Other:

* `fn __index__(&'p self) -> PyResult<impl ToPyObject>`

### 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).
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<T>` to implement yielding values during iteration.
In Python a generator can also return a value. To express this in Rust, PyO3 provides the
Expand Down
2 changes: 2 additions & 0 deletions pyo3-macros-backend/src/deprecations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub enum Deprecation {
PyfnNameArgument,
PyModuleNameArgument,
TextSignatureAttribute,
CallAttribute,
}

impl Deprecation {
Expand All @@ -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)
}
Expand Down

0 comments on commit 801b629

Please sign in to comment.