-
Notifications
You must be signed in to change notification settings - Fork 803
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
base: main
Are you sure you want to change the base?
Conversation
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__()"); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
??
There was a problem hiding this 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__()"); |
There was a problem hiding this comment.
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.
fn __del__(&mut self) { | ||
self._custom_attr = None; | ||
} |
There was a problem hiding this comment.
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.
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__()"); |
There was a problem hiding this comment.
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
??
pytests/tests/test_pyclasses.py
Outdated
def test_del_error(unraisablehook): | ||
d = pyclasses.PyClassDelError() | ||
del d | ||
assert unraisablehook.called |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
But PyObject_CallFinalizerFromDealloc
is only in the limited API, so it might be that we can only support __del__
when not running abi3
.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!)
I found today that if we don't implement However the documentation in this area is confusing, as it implies that 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. |
I spoke to vstinner (thought no need to ping so no @) briefly about this, I understood that |
Closes #2479