-
-
Notifications
You must be signed in to change notification settings - Fork 34.5k
[3.13] gh-105936: Properly update closure cells for __setattr__ and __delattr__ in frozen dataclasses with slots (GH-144021) #148476
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
Draft
gpshead
wants to merge
1
commit into
python:3.13
Choose a base branch
from
gpshead:backport-8a398bf-3.13
base: 3.13
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+121
−62
Draft
Changes from all commits
Commits
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 |
|---|---|---|
|
|
@@ -665,25 +665,25 @@ def _init_fn(fields, std_fields, kw_only_fields, frozen, has_post_init, | |
| return_type=None) | ||
|
|
||
|
|
||
| def _frozen_get_del_attr(cls, fields, func_builder): | ||
| locals = {'cls': cls, | ||
| def _frozen_set_del_attr(cls, fields, func_builder): | ||
| locals = {'__class__': cls, | ||
| 'FrozenInstanceError': FrozenInstanceError} | ||
| condition = 'type(self) is cls' | ||
| condition = 'type(self) is __class__' | ||
| if fields: | ||
| condition += ' or name in {' + ', '.join(repr(f.name) for f in fields) + '}' | ||
|
|
||
| func_builder.add_fn('__setattr__', | ||
| ('self', 'name', 'value'), | ||
| (f' if {condition}:', | ||
| ' raise FrozenInstanceError(f"cannot assign to field {name!r}")', | ||
| f' super(cls, self).__setattr__(name, value)'), | ||
| f' super(__class__, self).__setattr__(name, value)'), | ||
| locals=locals, | ||
| overwrite_error=True) | ||
| func_builder.add_fn('__delattr__', | ||
| ('self', 'name'), | ||
| (f' if {condition}:', | ||
| ' raise FrozenInstanceError(f"cannot delete field {name!r}")', | ||
| f' super(cls, self).__delattr__(name)'), | ||
| f' super(__class__, self).__delattr__(name)'), | ||
| locals=locals, | ||
| overwrite_error=True) | ||
|
|
||
|
|
@@ -1141,7 +1141,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, | |
| overwrite_error='Consider using functools.total_ordering') | ||
|
|
||
| if frozen: | ||
| _frozen_get_del_attr(cls, field_list, func_builder) | ||
| _frozen_set_del_attr(cls, field_list, func_builder) | ||
|
|
||
| # Decide if/how we're going to create a hash function. | ||
| hash_action = _hash_action[bool(unsafe_hash), | ||
|
|
@@ -1219,6 +1219,27 @@ def _get_slots(cls): | |
| raise TypeError(f"Slots of '{cls.__name__}' cannot be determined") | ||
|
|
||
|
|
||
| def _update_func_cell_for__class__(f, oldcls, newcls): | ||
| # Returns True if we update a cell, else False. | ||
| if f is None: | ||
| # f will be None in the case of a property where not all of | ||
| # fget, fset, and fdel are used. Nothing to do in that case. | ||
| return False | ||
| try: | ||
| idx = f.__code__.co_freevars.index("__class__") | ||
| except ValueError: | ||
| # This function doesn't reference __class__, so nothing to do. | ||
| return False | ||
| # Fix the cell to point to the new class, if it's already pointing | ||
| # at the old class. I'm not convinced that the "is oldcls" test | ||
| # is needed, but other than performance can't hurt. | ||
| closure = f.__closure__[idx] | ||
| if closure.cell_contents is oldcls: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This introduces a bug (on very contrived code): #148947. |
||
| closure.cell_contents = newcls | ||
| return True | ||
| return False | ||
|
|
||
|
|
||
| def _add_slots(cls, is_frozen, weakref_slot): | ||
| # Need to create a new class, since we can't set __slots__ | ||
| # after a class has been created. | ||
|
|
@@ -1260,18 +1281,37 @@ def _add_slots(cls, is_frozen, weakref_slot): | |
|
|
||
| # And finally create the class. | ||
| qualname = getattr(cls, '__qualname__', None) | ||
| cls = type(cls)(cls.__name__, cls.__bases__, cls_dict) | ||
| newcls = type(cls)(cls.__name__, cls.__bases__, cls_dict) | ||
| if qualname is not None: | ||
| cls.__qualname__ = qualname | ||
| newcls.__qualname__ = qualname | ||
|
|
||
| if is_frozen: | ||
| # Need this for pickling frozen classes with slots. | ||
| if '__getstate__' not in cls_dict: | ||
| cls.__getstate__ = _dataclass_getstate | ||
| newcls.__getstate__ = _dataclass_getstate | ||
| if '__setstate__' not in cls_dict: | ||
| cls.__setstate__ = _dataclass_setstate | ||
|
|
||
| return cls | ||
| newcls.__setstate__ = _dataclass_setstate | ||
|
|
||
| # Fix up any closures which reference __class__. This is used to | ||
| # fix zero argument super so that it points to the correct class | ||
| # (the newly created one, which we're returning) and not the | ||
| # original class. We can break out of this loop as soon as we | ||
| # make an update, since all closures for a class will share a | ||
| # given cell. | ||
| for member in newcls.__dict__.values(): | ||
| # If this is a wrapped function, unwrap it. | ||
| member = inspect.unwrap(member) | ||
|
|
||
| if isinstance(member, types.FunctionType): | ||
| if _update_func_cell_for__class__(member, cls, newcls): | ||
| break | ||
| elif isinstance(member, property): | ||
| if (_update_func_cell_for__class__(member.fget, cls, newcls) | ||
| or _update_func_cell_for__class__(member.fset, cls, newcls) | ||
| or _update_func_cell_for__class__(member.fdel, cls, newcls)): | ||
| break | ||
|
|
||
| return newcls | ||
|
|
||
|
|
||
| def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False, | ||
|
|
||
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
5 changes: 5 additions & 0 deletions
5
Misc/NEWS.d/next/Library/2026-01-19-21-23-18.gh-issue-105936.dGrzjM.rst
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,5 @@ | ||
| Attempting to mutate non-field attributes of :mod:`dataclasses` | ||
| with both *frozen* and *slots* being ``True`` now raises | ||
| :class:`~dataclasses.FrozenInstanceError` instead of :class:`TypeError`. | ||
| Their non-dataclass subclasses can now freely mutate non-field attributes, | ||
| and the original non-slotted class can be garbage collected. |
Oops, something went wrong.
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.
It can make a difference if you assign a method from another class in the class body of the dataclass. I don't know why you'd do that, but maybe there's a good reason for it somewhere.