Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace (A)IterNextOutput by autoref-based specialization to allow returning arbitrary value #3661

Merged
merged 5 commits into from
Dec 20, 2023

Conversation

adamreichold
Copy link
Member

@adamreichold adamreichold commented Dec 18, 2023

To be done:

  • changelog entry
  • migration guide entry

Closes #3190

@adamreichold adamreichold added the CI-skip-changelog Skip checking changelog entry label Dec 18, 2023
@davidhewitt
Copy link
Member

Fantastic! I am a little tight for time today, I'll do my best to give this a full read tomorrow 👍

@davidhewitt
Copy link
Member

I've had a read and makes sense, I'll hold off from too many comments on the diff while we're figuring out the design.

I would like to suggest we don't deprecate the Option<T> special case. As per #3190 (comment) I think we can use the None variant for efficiency of returning NULL. I also think the fact that Rust iterators return Option will make this specialization very attractive in user code.

And I guess if we did keep that, probably PyResult<Option<T>> also needs to specialise in the same way? Rust fallible iterators of Option<Result<...>> can probably then just .transpose() away the error. Or would we want to specialise Option<Result<..>> directly? 🤔

@adamreichold
Copy link
Member Author

I would like to suggest we don't deprecate the Option special case.

Yeah, I was leaning into direction as well after seeing how little changes are necessary to make this work for existing code. And removing IterNextOutput completely should ensure compilation errors where semantics need to be adjusted.

As per #3190 (comment) I think we can use the None variant for efficiency of returning NULL.

If I understand this correctly, I would need to remove the generic Target and enforce *mut ffi::PyObject for that to work which should work since the specialization is used only for this single slot type?

And I guess if we did keep that, probably PyResult<Option> also needs to specialise in the same way? Rust fallible iterators of Option<Result<...>> can probably then just .transpose() away the error. Or would we want to specialise Option<Result<..>> directly? 🤔

I already added PyResult<Option<T>>. I think adding Option<Result<..>> would require a second layer of specialization and be even harder to explain, so I would like to avoid adding it and defer Rust users to transpose (which suitable guide entries). Especially since even the conversion trait bounds themselves will be a bit messy due to the need to convert the error type as most iterators will probably not yield PyErr directly.

@davidhewitt
Copy link
Member

If you are not fond of specialization introducing subtle behavioural difference based on return type, maybe rather than specialize on Option<T> we could have our own enum to specialize on, with handy traits to convert options and options-of-results to it? (And deprecate the Option<T> pathway as you currently have done here)

@davidhewitt
Copy link
Member

If I understand this correctly, I would need to remove the generic Target and enforce *mut ffi::PyObject for that to work which should work since the specialization is used only for this single slot type?

I think so, yes. Given the tags only work for this slot, I can't see a potential downside right now, and as a performance optimization I guess we can back out again if it's problematic.

@davidhewitt
Copy link
Member

I already added PyResult<Option<T>>. I think adding Option<Result<..>> would require a second layer of specialization and be even harder to explain, so I would like to avoid adding it and defer Rust users to transpose (which suitable guide entries). Especially since even the conversion trait bounds themselves will be a bit messy due to the need to convert the error type as most iterators will probably not yield PyErr directly.

👍 Makes sense to me!

@adamreichold
Copy link
Member Author

If you are not fond of specialization introducing subtle behavioural difference based on return type, maybe rather than specialize on Option<T> we could have our own enum to specialize on, with handy traits to convert options and options-of-results to it? (And deprecate the Option<T> pathway as you currently have done here)

Yes, I am not fond of that. But the resulting usability and flexibility is a strong argument for supporting Option directly in this manner. And with really one special case, I think it is mainly a question of documentation which I will need to overhaul in any case after we settled design and implementation.

@adamreichold adamreichold changed the title WIP: Drop IterNextOutput and deprecate __next__ returing Option Replace (A)IterNextOutput by autoref-based specialization to allow returning arbitrary value Dec 19, 2023
src/lib.rs Outdated Show resolved Hide resolved
src/pyclass/mod.rs Outdated Show resolved Hide resolved
pyo3-macros-backend/src/pymethod.rs Outdated Show resolved Hide resolved
@adamreichold
Copy link
Member Author

Turns out I did not fully wire the holders change through. Added this as a separate commit here and now the modified test case does indeed compile.

@adamreichold adamreichold removed the CI-skip-changelog Skip checking changelog entry label Dec 19, 2023
Copy link
Member

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good!

pyo3-macros-backend/src/method.rs Outdated Show resolved Hide resolved
src/callback.rs Outdated Show resolved Hide resolved
@davidhewitt davidhewitt mentioned this pull request Dec 20, 2023
@adamreichold adamreichold marked this pull request as ready for review December 20, 2023 11:22
Copy link

codspeed-hq bot commented Dec 20, 2023

CodSpeed Performance Report

Merging #3661 will improve performances by 22.03%

Comparing iter-output-type (5528895) with main (3583b9a)

Summary

⚡ 1 improvements
✅ 77 untouched benchmarks

Benchmarks breakdown

Benchmark main iter-output-type Change
list_via_downcast 153.9 ns 126.1 ns +22.03%

Copy link
Member

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! I have just one suggestion on wording in the guide, and also a thought on whether we can/should generalise the error type for the result specialisations.

guide/src/migration.md Outdated Show resolved Hide resolved
src/impl_/pymethods.rs Outdated Show resolved Hide resolved
@adamreichold
Copy link
Member Author

(I guess at some point in the future, we'd want to do something similar for function arguments to avoid matching on the "Option" name?)

@davidhewitt
Copy link
Member

Quite possibly, yes. I played around with something to that regard in the past, but I think the patch is now lost/dead. Also I believe that to completely stop matching on "Option" as a text string, we would need to proceed with #2935

Copy link
Member

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fantastic, thanks! It's very pleasing to see that this makes user code read nicer :)

@adamreichold adamreichold added this pull request to the merge queue Dec 20, 2023
Merged via the queue into main with commit 1b3dc6d Dec 20, 2023
35 checks passed
@adamreichold adamreichold deleted the iter-output-type branch December 20, 2023 13:57
@gi0baro
Copy link
Contributor

gi0baro commented Feb 2, 2024

@davidhewitt Just noticed this and I have a question in regards to the deprecation: is there a way to yield None (also known as bare yielding) without using IterNextOutput?

For instance, how this code could be translated to avoid IterNextOutput usage?

#[pyclass]
pub(crate) struct PyIterAwaitable {
    result: Option<PyResult<PyObject>>,
}

impl PyIterAwaitable {
    pub(crate) fn new() -> Self {
        Self { result: None }
    }

    pub(crate) fn set_result(&mut self, result: PyResult<PyObject>) {
        self.result = Some(result);
    }
}

#[pymethods]
impl PyIterAwaitable {
    fn __await__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> {
        pyself
    }

    fn __iter__(pyself: PyRef<'_, Self>) -> PyRef<'_, Self> {
        pyself
    }

    fn __next__(&self, py: Python) -> PyResult<IterNextOutput<PyObject, PyObject>> {
        match &self.result {
            Some(res) => match res {
                Ok(v) => Ok(IterNextOutput::Return(v.clone_ref(py))),
                Err(err) => Err(err.clone_ref(py)),
            },
            _ => Ok(IterNextOutput::Yield(py.None())),
        }
    }
}

@davidhewitt
Copy link
Member

@gi0baro great question!

To update, you'd want to directly return py.None() from __next__, and when you're ready to Return, raise a StopIteration error like how any other exception is raised by PyO3:

    fn __next__(&self, py: Python) -> PyResult<PyObject> {
        match &self.result {
            Some(res) => match res {
                Ok(v) => Ok(PyStopIteration::new_err(v.clone_ref(py)).into_py(py)),
                Err(err) => Err(err.clone_ref(py)),
            },
            _ => Ok(py.None().into_py(py)),
        }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

__anext__ should be able to return &PyAny
3 participants