-
-
Notifications
You must be signed in to change notification settings - Fork 390
Fix slotted reference cycles on 3.14 #1446
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
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
ea6a62d
Fix slotted reference cycles on 3.14
hynek 8e26783
Add news fragment
hynek c60dfa0
Bump version tag to CMA
hynek 98af24a
Apply review comment
hynek 456253a
EAFP
hynek ce806dc
Merge branch 'main' into fix-slots-314
hynek File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
On 3.14, the cycles in slotted classes are now manually broken. | ||
An explicit call to `gc.collect()` is still necessary, unfortunately. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
I'd recommend putting all this in a try-except in case we break this trick in a later version of CPython; if so, it feels better for your users to leak the original class than to get a mysterious error about mappingproxies.
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.
@JelleZijlstra given context from Discord, we’re kinda weighting “broken introspection” vs “things explode if someone adds another decorator at the wrong position” anyways?
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, this variant definitely has some questionable edge cases. I think you can be a bit less conservative in CPython in terms of judging whether this hack is worth it.
As I understand from Brandt, a problem with the hack is that it mutates the class dict without incrementing the internal version tag for the type object. A workaround could then be to perform some no-op that increments the version tag anyway. For example, setting
cls.__abstractmethods__
to its existing value could work.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.
I'm afraid we have to be more conservative, because in attrs slots are on by default. :|
But looking again at the code, which part is actually dangerous here? It's just the wekreaf part, no? We do not modify
self._cls.__dict__
– we just use your escape hatch to break cycles.E.g. I tried this:
and it obviously fails, because there's a legit reference to the original class within tmp. But both prints work and print what would be expected without segfaulting.
Doesn't this mean that the only downside/edge case (aside from it potentially breaking eventually) is that the original class can't be weakref'ed? I'm not super deep in this topic, so happy to be enlightened. That would def be a downside I'd be OK to accept.
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.
No, the risky part is removing
"__dict__"
from the__dict__
. The interpreter uses various caches and optimizations that are keyed on the "type version", a number that gets incremented when the type is modified. Because we use this trick to get to the type dict (which usually can't be modified directly from Python code), we're modifying the type dict without bumping the version number. That can lead to crashes if interpreter code assumes the dict is unchanged from before.There is a separate risk that you break users of the original class who rely on the
__dict__
/__weakref__
slots. But that is Python-level breakage (things throw exceptions), not C-level breakage (the interpreter crashes).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.
A bit more color on what exactly this breaks on the affected class (the original class that we want to be garbage collected):
__weakref__
actually breaks very little that matters. Even after the slot is deleted,weakref.ref()
still works on these objects. It's just the Python-visibleobj.__weakref__
attribute that is gone, but as far as I can tell basically nothing relies on that.__dict__
means thatobj.__dict__
no longer works, but direct attribute access (obj.x = 1
) still works.