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

pymethods: add support for __del__ #2484

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft

Conversation

messense
Copy link
Member

Closes #2479

Python::with_gil(|py| {
let example_py = make_example(py);
example_py.setattr("special_custom_attr", 15).unwrap();
py_run!(py, example_py, "example_py.__del__()");
Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure what's the best way to test it.

It looks like the tp_dealloc in that file also calls tp_finalize in some circumstances; we might need to check carefully that we reproduce the Python behaviour correctly.

@davidhewitt Could you elaborate on how do we check that?

Copy link
Member

Choose a reason for hiding this comment

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

Good questions.

For testing, you could also potentially just let the reference count of the object go to zero at which point I think all versions of CPython which we support will call tp_finalize pretty much immediately.

In pytests module it might be worth writing a Python and Rust version of the same class and confirming that their __del__ method works the same.

Note also that the Python docs mention that __del__ can increase the reference count of the object and "resurrect" it. It might be worth adding a test that this works correctly? Not sure.

Copy link
Member

Choose a reason for hiding this comment

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

For tp_dealloc - I note that here in the CPython tp_dealloc implementation for Python classes it calls tp_finalize. We might need to modify the tp_dealloc we have in our PyCell implementation to do this.

...or maybe we should be removing our custom tp_dealloc and moving all logic to tp_finalize ??

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.

Thanks! Please also add a CHANGELOG and add to protocols.md in the guide.

Note that also on Python 3.7 we need to set Py_TPFLAGS_HAVE_FINALIZE, it's ignored on newer versions.

Python::with_gil(|py| {
let example_py = make_example(py);
example_py.setattr("special_custom_attr", 15).unwrap();
py_run!(py, example_py, "example_py.__del__()");
Copy link
Member

Choose a reason for hiding this comment

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

Good questions.

For testing, you could also potentially just let the reference count of the object go to zero at which point I think all versions of CPython which we support will call tp_finalize pretty much immediately.

In pytests module it might be worth writing a Python and Rust version of the same class and confirming that their __del__ method works the same.

Note also that the Python docs mention that __del__ can increase the reference count of the object and "resurrect" it. It might be worth adding a test that this works correctly? Not sure.

Comment on lines +65 to +67
fn __del__(&mut self) {
self._custom_attr = None;
}
Copy link
Member

Choose a reason for hiding this comment

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

As per CPython docs, exceptions in __del__ are ignored and print a warning. It would be cool to check that if we return PyResult we get the same behaviour. We could add a test to the pytests crate which checks that sys.unraiseablehook is called, I think.

https://github.com/python/cpython/blob/71868a0066a90519ccc6aeb75673ae882aaa03f0/Objects/typeobject.c#L8047

Python::with_gil(|py| {
let example_py = make_example(py);
example_py.setattr("special_custom_attr", 15).unwrap();
py_run!(py, example_py, "example_py.__del__()");
Copy link
Member

Choose a reason for hiding this comment

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

For tp_dealloc - I note that here in the CPython tp_dealloc implementation for Python classes it calls tp_finalize. We might need to modify the tp_dealloc we have in our PyCell implementation to do this.

...or maybe we should be removing our custom tp_dealloc and moving all logic to tp_finalize ??

def test_del_error(unraisablehook):
d = pyclasses.PyClassDelError()
del d
assert unraisablehook.called
Copy link
Member Author

Choose a reason for hiding this comment

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

This test case does not pass locally, adding a panic!() to __del__ seems to indicate that __del__ is not called as it did not trigger a panic.

Copy link
Member

Choose a reason for hiding this comment

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

As per #2484 (comment) this indicates to me that we may want to consider adding a block similar to the following to PyCell::tp_dealloc

https://github.com/python/cpython/blob/56310136170ef6653cb050f58dd2d32538997f59/Modules/_asynciomodule.c#L1534-L1542

But PyObject_CallFinalizerFromDealloc is only in the limited API, so it might be that we can only support __del__ when not running abi3.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's unfortunate.

...or maybe we should be removing our custom tp_dealloc and moving all logic to tp_finalize ??

Could we do this instead so it does not require calling PyObject_CallFinalizerFromDealloc in extension module?

Copy link
Member

@davidhewitt davidhewitt Jul 7, 2022

Choose a reason for hiding this comment

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

Yes, that seems reasonable to me. I have no idea whether it will definitely work. I plan to play around with this once the 0.17 release is complete. (Feel free to experiment sooner if you want!)

@davidhewitt
Copy link
Member

davidhewitt commented May 27, 2023

I found today that if we don't implement tp_dealloc then the default tp_dealloc implementation (at least for CPython) will call tp_finalize. This gives us a potential route to support this and also fix #3064 at the same time.

However the documentation in this area is confusing, as it implies that tp_dealloc doesn't have a default - python/cpython#105018 / https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_dealloc

I think it may be possible that we use that default implementation to support this case, but we'll need to test carefully whether this also works for all Python versions, PyPy etc.

@davidhewitt davidhewitt mentioned this pull request Jun 16, 2023
6 tasks
@davidhewitt davidhewitt added this to the 0.20 milestone Aug 11, 2023
@davidhewitt davidhewitt modified the milestones: 0.20, 0.22 Oct 2, 2023
@davidhewitt
Copy link
Member

I spoke to vstinner (thought no need to ping so no @) briefly about this, I understood that PyObject_CallFinalizerFromDealloc could be added to the limited API in 3.13. So maybe we can figure out a way to proceed with this where __del__ is supported without abi3 for now and then we can add abi3 support on 3.13.

@davidhewitt davidhewitt modified the milestones: 0.22, 0.24 Oct 4, 2024
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.

Support __del__
2 participants