GeezerVison is showing github.com — gh-90562: Improve zero argument support for `super()` in dataclasses when `slots=True` by Bobronium · Pull Request #124692 · python/cpython
gh-90562: Improve zero argument support for super() in dataclasses when slots=True
#124692
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
Uh oh!
There was an error while loading. Please reload this page.
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
475794b
Improve zero argument support for `super()` in dataclasses
Bobronium Sep 27, 2024
75e769d
Fix NEWS.d formatting
Bobronium Sep 27, 2024
3c09e3a
Add explanation for _depth use and tests
Bobronium Sep 27, 2024
d53d538
Fix incorrect variable reference
Bobronium Sep 27, 2024
a9c4319
Correctly unwrap descriptor function
Bobronium Sep 27, 2024
26e13e7
Add test with using partial as custom wrapper
Bobronium Sep 27, 2024
3a08b0e
Rephrase comment explaining _depth argument
Bobronium Sep 27, 2024
2e9588d
Add clarification for function search priority
Bobronium Sep 27, 2024
fb4bc9f
Ues consistent grammar throughout comments
Bobronium Sep 27, 2024
bdaeb1f
Fix breaking out from outer loop when handling property
Bobronium Sep 27, 2024
8f6140a
Use generator expression with any() instead of classic loop
Bobronium Sep 27, 2024
13c2dc7
Apply suggestions from code review
Bobronium Sep 28, 2024
9d3d49f
Remove leading underscores from seen and depth args
Bobronium Sep 28, 2024
d095864
Prevent user-defined code execution during attribute scanning
Bobronium Sep 28, 2024
6848c1b
Use inspect.getattr_static instead of _safe_get_attributes
Bobronium Oct 5, 2024
1d443e7
Put back the comment in _update_func_cell_for__class__
Bobronium Oct 5, 2024
5a7037b
Rely on inspect.getmembers_static()
Bobronium Oct 5, 2024
File filter
Filter by extension
.py
(2)
.rst
(1)
All 2 file types selected
Viewed files
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Hide whitespace
Diff view
Hide whitespace
There are no files selected for viewing
Original file line number
Diff line number
Diff line change
Expand Up
@@ -1290,10 +1290,6 @@ def _get_slots(cls):
def _update_func_cell_for__class__(f, oldcls, newcls):
# Returns True if we update a cell, else False.
Comment thread
Bobronium marked this conversation as resolved.
Resolved
Uh oh!
There was an error while loading. Please reload this page.
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:
Expand All
@@ -1317,6 +1313,58 @@ def _update_func_cell_for__class__(f, oldcls, newcls):
return False
_object_members_values = None
def _is_not_object_member(v):
# _object_members_values is initialized on first use, so that
# importing dataclasses does not eagerly import inspect (gh-137855).
global _object_members_values
if _object_members_values is None:
import inspect
_object_members_values = {
value for _, value in
(
*inspect.getmembers_static(object),
*inspect.getmembers_static(object())
)
}
try:
return v not in _object_members_values
except TypeError:
return True
def _find_inner_functions(obj, seen=None, depth=0):
# inspect is imported lazily (see gh-137855).
import inspect
if seen is None:
seen = set()
if id(obj) in seen:
return None
seen.add(id(obj))
depth += 1
# Normally just an inspection of a descriptor object itself should be enough,
# and we should encounter the function as its attribute,
# but in case function was wrapped (e.g. functools.partial was used),
# we want to dive at least one level deeper.
if depth > 2:
return None
obj_is_type_instance = type in inspect._static_getmro(type(obj))
for _, value in inspect.getmembers_static(obj, _is_not_object_member):
value_type = type(value)
if value_type is types.MemberDescriptorType and not obj_is_type_instance:
value = value.__get__(obj)
value_type = type(value)
if value_type is types.FunctionType:
yield inspect.unwrap(value)
else:
yield from _find_inner_functions(value, seen, depth)
def _create_slots(defined_fields, inherited_slots, field_names, weakref_slot):
# The slots for our class. Remove slots from our base classes. Add
# '__weakref__' if weakref_slot was given, unless it is already present.
Expand Down
Expand Up
@@ -1391,22 +1439,45 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
# (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.
# given cell. First we try to find a pure function or a property,
# and then fallback to inspecting custom descriptors
# if no pure function or property is found.
# inspect is imported lazily (see gh-137855): only slots classes
# with wrapped members, properties or descriptors need it here.
import inspect
custom_descriptors_to_check = []
for member in newcls.__dict__.values():
# If this is a wrapped function, unwrap it.
if not isinstance(member, type) and hasattr(member, '__wrapped__'):
import inspect
member = inspect.unwrap(member)
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
elif isinstance(member, property) and (
any(
# Unwrap once more in case function
# was wrapped before it became property.
_update_func_cell_for__class__(inspect.unwrap(f), cls, newcls)
for f in (member.fget, member.fset, member.fdel)
if f is not None
)
):
break
elif hasattr(member, "__get__") and not inspect.ismemberdescriptor(
member
):
# We don't want to inspect custom descriptors just yet
# there's still a chance we'll encounter a pure function
# or a property and won't have to use slower recursive search.
custom_descriptors_to_check.append(member)
else:
# Now let's ensure custom descriptors won't be left out.
for descriptor in custom_descriptors_to_check:
for f in _find_inner_functions(descriptor):
if _update_func_cell_for__class__(f, cls, newcls):
break
# Get new annotations to remove references to the original class
# in forward references
Expand Down
Original file line number
Diff line number
Diff line change
Expand Up
@@ -16,6 +16,7 @@
import sys
import textwrap
import unittest
from functools import partial, update_wrapper
from unittest.mock import Mock
from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional, Protocol, DefaultDict
from typing import get_type_hints
Expand Down
Expand Up
@@ -5533,6 +5534,194 @@ def foo(self):
A().foo()
def test_wrapped_property(self):
def mydecorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
return wrapper
class B:
@property
def foo(self):
return "bar"
@dataclass(slots=True)
class A(B):
@property
@mydecorator
def foo(self):
return super().foo
self.assertEqual(A().foo, "bar")
def test_custom_descriptor(self):
class CustomDescriptor:
def __init__(self, f):
self._f = f
def __get__(self, instance, owner):
return self._f(instance)
class B:
def foo(self):
return "bar"
@dataclass(slots=True)
class A(B):
@CustomDescriptor
def foo(cls):
return super().foo()
self.assertEqual(A().foo, "bar")
def test_custom_descriptor_wrapped(self):
class CustomDescriptor:
def __init__(self, f):
self._f = update_wrapper(lambda *args, **kwargs: f(*args, **kwargs), f)
def __get__(self, instance, owner):
return self._f(instance)
class B:
def foo(self):
return "bar"
@dataclass(slots=True)
class A(B):
@CustomDescriptor
def foo(cls):
return super().foo()
self.assertEqual(A().foo, "bar")
def test_custom_nested_descriptor(self):
class CustomFunctionWrapper:
def __init__(self, f):
self._f = f
def __call__(self, *args, **kwargs):
return self._f(*args, **kwargs)
class CustomDescriptor:
def __init__(self, f):
self._wrapper = CustomFunctionWrapper(f)
def __get__(self, instance, owner):
return self._wrapper(instance)
class B:
def foo(self):
return "bar"
@dataclass(slots=True)
class A(B):
@CustomDescriptor
def foo(cls):
return super().foo()
self.assertEqual(A().foo, "bar")
def test_custom_nested_descriptor_with_partial(self):
class CustomDescriptor:
def __init__(self, f):
self._wrapper = partial(f, value="bar")
def __get__(self, instance, owner):
return self._wrapper(instance)
class B:
def foo(self, value):
return value
@dataclass(slots=True)
class A(B):
@CustomDescriptor
def foo(self, value):
return super().foo(value)
self.assertEqual(A().foo, "bar")
def test_custom_too_nested_descriptor(self):
class UnnecessaryNestedWrapper:
def __init__(self, wrapper):
self._wrapper = wrapper
def __call__(self, *args, **kwargs):
return self._wrapper(*args, **kwargs)
class CustomFunctionWrapper:
def __init__(self, f):
self._f = f
def __call__(self, *args, **kwargs):
return self._f(*args, **kwargs)
class CustomDescriptor:
def __init__(self, f):
self._wrapper = UnnecessaryNestedWrapper(CustomFunctionWrapper(f))
def __get__(self, instance, owner):
return self._wrapper(instance)
class B:
def foo(self):
return "bar"
@dataclass(slots=True)
class A(B):
@CustomDescriptor
def foo(cls):
return super().foo()
with self.assertRaises(TypeError) as context:
A().foo
expected_error_message = (
'super(type, obj): obj (instance of A) is not '
'an instance or subtype of type (A).'
)
self.assertEqual(context.exception.args, (expected_error_message,))
def test_user_defined_code_execution(self):
class CustomDescriptor:
def __init__(self, f):
self._wrapper = partial(f, value="bar")
def __get__(self, instance, owner):
return object.__getattribute__(self, "_wrapper")(instance)
def __getattribute__(self, name):
if name in {
# these are the bare minimum for the feature to work
"__class__", # accessed on `isinstance(value, Field)`
"__wrapped__", # accessed by unwrap
"__get__", # is required for the descriptor protocol
"__dict__", # is accessed by dir() to work
}:
return object.__getattribute__(self, name)
raise RuntimeError(f"Never should be accessed: {name}")
class B:
def foo(self, value):
return value
@dataclass(slots=True)
class A(B):
@CustomDescriptor
def foo(self, value):
return super().foo(value)
self.assertEqual(A().foo, "bar")
@dataclass(slots=True)
class A(B):
@CustomDescriptor
def foo(self, value):
return super().foo(value)
self.assertEqual(A().foo, "bar")
def test_remembered_class(self):
# Apply the dataclass decorator manually (not when the class
# is created), so that we can keep a reference to the
Expand Down
Original file line number
Diff line number
Diff line change
@@ -0,0 +1,2 @@
Modify dataclasses to enable zero argument support for ``super()`` when ``slots=True`` is
specified and custom descriptor is used or ``property`` function is wrapped.
Loading
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
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.