diff --git a/guide/src/migration.md b/guide/src/migration.md index 0c55c15ce09..15de578e948 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -74,7 +74,124 @@ Python::with_gil(|py| { }); ``` -### `PyType::name` is now `PyType::qualname` +### `Iter(A)NextOutput` are deprecated + +The `IterNextOutput` used by `__next__` and `IterANextOutput` used by `__anext__` are deprecated. These slots can now return any type convertible into Python objects direclty using the same mechanism as other functions and methods implemented using PyO3. Most importantly, this allows returning an awaitable from `__anext__` without non-sensically wrapping it into `Yield` or `Some`. Only the return types `Option` and `PyResult>` are still handled in special manner where `Some(val)` yields `val` and `None` stops iteration. + +Starting with an implementation of a Python iterator using `IterNextOutput`, e.g. + +```rust +#![allow(deprecated)] +use pyo3::prelude::*; +use pyo3::iter::IterNextOutput; + +#[pyclass] +struct PyClassIter { + count: usize, +} + +#[pymethods] +impl PyClassIter { + fn __next__(&mut self) -> IterNextOutput { + if self.count < 5 { + self.count += 1; + IterNextOutput::Yield(self.count) + } else { + IterNextOutput::Return("done") + } + } +} +``` + +If returning `"done"` via `StopIteration` is not really required, this should be written as + +```rust +use pyo3::prelude::*; + +#[pyclass] +struct PyClassIter { + count: usize, +} + +#[pymethods] +impl PyClassIter { + fn __next__(&mut self) -> Option { + if self.count < 5 { + self.count += 1; + Some(self.count) + } else { + None + } + } +} +``` + +This form also has additional benefits: It has already worked in previous PyO3 versions, it matches the signature of Rust's [`Iterator` trait](https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html) and it allows using a fast path in CPython which completely avoids the cost of raising a `StopIteration` exception. Note that using [`Option::transpose`](https://doc.rust-lang.org/stable/std/option/enum.Option.html#method.transpose) and the `PyResult>` variant, this form can also be used to wrap fallible iterators. + +Alternatively, the implementation can also be done as it would in Python itself, i.e. by "raising" a `StopIteration` exception + +```rust +use pyo3::prelude::*; +use pyo3::exceptions::PyStopIteration; + +#[pyclass] +struct PyClassIter { + count: usize, +} + +#[pymethods] +impl PyClassIter { + fn __next__(&mut self) -> PyResult { + if self.count < 5 { + self.count += 1; + Ok(self.count) + } else { + Err(PyStopIteration::new_err("done")) + } + } +} +``` + +Finally, an asynchronous iterator can directly return an awaitable without confusing wrapping + +```rust +use pyo3::prelude::*; + +#[pyclass] +struct PyClassAwaitable { + number: usize, +} + +#[pymethods] +impl PyClassAwaitable { + fn __next__(&self) -> usize { + self.number + } + + fn __await__(slf: Py) -> Py { + slf + } +} + +#[pyclass] +struct PyClassAsyncIter { + number: usize, +} + +#[pymethods] +impl PyClassAsyncIter { + fn __anext__(&mut self) -> PyClassAwaitable { + self.number += 1; + PyClassAwaitable { number: self.number } + } + + fn __aiter__(slf: Py) -> Py { + slf + } +} +``` + +### `PyType::name` has been renamed to `PyType::qualname` `PyType::name` has been renamed to `PyType::qualname` to indicate that it does indeed return the [qualified name](https://docs.python.org/3/glossary.html#term-qualified-name), matching the `__qualname__` attribute. The newly added `PyType::name` yields the full name including the module name now which corresponds to `__module__.__name__` on the level of attributes. diff --git a/newsfragments/3661.changed.md b/newsfragments/3661.changed.md new file mode 100644 index 00000000000..8245a6f1a80 --- /dev/null +++ b/newsfragments/3661.changed.md @@ -0,0 +1 @@ +The `Iter(A)NextOutput` types are now deprecated and `__(a)next__` can directly return anything which can be converted into Python objects, i.e. awaitables do not need to be wrapped into `IterANextOutput` or `Option` any more. `Option` can still be used as well and returning `None` will trigger the fast path for `__next__`, stopping iteration without having to raise a `StopIteration` exception.