diff --git a/guide/src/migration.md b/guide/src/migration.md index 0c55c15c..b002daf9 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 `__next__` and `__anext__` magic methods can now return any type convertible into Python objects directly just like all other `#[pymethods]`. The `IterNextOutput` used by `__next__` and `IterANextOutput` used by `__anext__` are subsequently deprecated. Most importantly, this change 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 a 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 00000000..8245a6f1 --- /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.