Skip to content

Commit fd270a2

Browse files
committed
Merge pull request #4519 in G/graalpython from ss/slots-tp_descrget-compat-GR-53082 to master
* commit '0dd805aee99f11b59285b0fa27c8e8af6d1f75ba': Align method attribute lookup with CPython GR-53090 Align method descriptor behavior with CPython Align classmethod descriptor behavior with CPython
2 parents adb460a + 0dd805a commit fd270a2

11 files changed

Lines changed: 379 additions & 127 deletions

File tree

graalpython/com.oracle.graal.python.test/src/tests/test_call-classmethod.py

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2018, 2021, Oracle and/or its affiliates.
1+
# Copyright (c) 2018, 2026, Oracle and/or its affiliates.
22
# Copyright (c) 2013, Regents of the University of California
33
#
44
# All rights reserved.
@@ -47,3 +47,137 @@ def test_strength():
4747
assert not Strength.weaker(s1, s2)
4848
assert s1.stronger(s1, s2)
4949
assert not s1.weaker(s1, s2)
50+
51+
52+
def test_classmethod_wraps_descriptor():
53+
class Descriptor:
54+
def __get__(self, obj, typ=None):
55+
return obj, typ
56+
57+
class C:
58+
method = classmethod(Descriptor())
59+
60+
assert C.method == (C, C)
61+
assert C().method == (C, C)
62+
63+
method = C.__dict__["method"]
64+
assert method.__get__(None, C) == (C, C)
65+
assert method.__get__(C()) == (C, C)
66+
67+
68+
def test_classmethod_wraps_property():
69+
class C:
70+
@classmethod
71+
@property
72+
def name(cls):
73+
return cls.__name__
74+
75+
class D(C):
76+
pass
77+
78+
assert C.name == "C"
79+
assert C().name == "C"
80+
assert D.name == "D"
81+
assert D().name == "D"
82+
83+
84+
def test_classmethod_wraps_staticmethod():
85+
class C:
86+
method = classmethod(staticmethod(lambda value: ("static", value)))
87+
88+
assert C.method("arg") == ("static", "arg")
89+
assert C().method("arg") == ("static", "arg")
90+
91+
92+
def test_classmethod_wraps_classmethod():
93+
class C:
94+
def method(cls, value):
95+
return cls, value
96+
97+
method = classmethod(classmethod(method))
98+
99+
class D(C):
100+
pass
101+
102+
assert C.method("arg") == (C, "arg")
103+
assert C().method("arg") == (C, "arg")
104+
assert D.method("arg") == (D, "arg")
105+
assert D().method("arg") == (D, "arg")
106+
107+
108+
def test_classmethod_wraps_bound_method():
109+
class C:
110+
def method(self, cls):
111+
return self, cls
112+
113+
receiver = C()
114+
assert not hasattr(type(receiver.method), "__get__")
115+
116+
class D:
117+
method = classmethod(receiver.method)
118+
119+
assert D.method() == (receiver, D)
120+
assert D().method() == (receiver, D)
121+
122+
123+
def test_classmethod_descriptor_get_errors():
124+
descriptor = dict.__dict__["fromkeys"]
125+
126+
assert descriptor.__get__(None, dict)([1, 2]) == {1: None, 2: None}
127+
assert descriptor.__get__({})([1, 2]) == {1: None, 2: None}
128+
129+
for args in ((None, None), (42,), (None, 42), (None, int), ({}, int)):
130+
try:
131+
descriptor.__get__(*args)
132+
except TypeError:
133+
pass
134+
else:
135+
raise AssertionError("classmethod_descriptor.__get__ accepted invalid arguments")
136+
137+
138+
def test_classmethod_descriptor_get_uses_object_type_when_type_omitted():
139+
class MyDict(dict):
140+
pass
141+
142+
descriptor = dict.__dict__["fromkeys"]
143+
bound = descriptor.__get__(MyDict())
144+
assert bound.__self__ is MyDict
145+
assert type(bound([1, 2])) is MyDict
146+
147+
148+
def test_classmethod_descriptor_get_does_not_keep_type_alive():
149+
import time
150+
from test import support
151+
152+
descriptor = object.__dict__["__init_subclass__"]
153+
154+
class Parent:
155+
pass
156+
157+
class Child(Parent):
158+
pass
159+
160+
bound = descriptor.__get__(None, Child)
161+
del bound
162+
assert Parent.__subclasses__() == [Child]
163+
164+
del Child
165+
for _ in range(100):
166+
support.gc_collect()
167+
if not Parent.__subclasses__():
168+
break
169+
if getattr(support, "is_graalpy", False):
170+
time.sleep(0.1)
171+
assert Parent.__subclasses__() == []
172+
173+
174+
def test_cext_classmethod_descriptor():
175+
from _ctypes import _SimpleCData
176+
177+
class c_void_p(_SimpleCData):
178+
_type_ = "P"
179+
180+
descriptor = c_void_p.__dict__["from_param"]
181+
assert type(descriptor).__name__ == "classmethod_descriptor"
182+
assert callable(descriptor.__get__(None, c_void_p))
183+
c_void_p.from_param(0)

graalpython/com.oracle.graal.python.test/src/tests/test_methods.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,35 @@ def g(self):
107107
assert types.MethodType(f, A).__qualname__ == "test_method_qualname_uses_wrapped_callable.<locals>.f"
108108
assert A.m.__qualname__ == "test_method_qualname_uses_wrapped_callable.<locals>.f"
109109
assert A.__dict__["m"].__qualname__ == "test_method_qualname_uses_wrapped_callable.<locals>.f"
110+
111+
112+
def test_method_getattribute_does_not_swallow_method_descriptor_attribute_error():
113+
import types
114+
115+
class Callable:
116+
def __init__(self):
117+
self.name_calls = 0
118+
119+
def __call__(self, *args):
120+
pass
121+
122+
def __getattribute__(self, name):
123+
if name == "__name__":
124+
calls = object.__getattribute__(self, "name_calls")
125+
object.__setattr__(self, "name_calls", calls + 1)
126+
if calls == 0:
127+
raise AttributeError("first lookup failure")
128+
return "fallback-name"
129+
return object.__getattribute__(self, name)
130+
131+
func = Callable()
132+
method = types.MethodType(func, object())
133+
try:
134+
method.__name__
135+
except AttributeError as e:
136+
assert str(e) == "first lookup failure"
137+
else:
138+
assert False, "AttributeError was not raised"
139+
assert func.name_calls == 1
140+
assert method.__class__ is types.MethodType
141+
assert method.__name__ == "fallback-name"

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextDescrBuiltins.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ public static long GraalPyPrivate_Descr_NewClassMethod(long methodDefPtr, long n
104104
Object type = NativeToPythonClassInternalNode.executeUncached(typeRaw);
105105
PBuiltinFunction func = MethodDescriptorWrapper.createWrapperFunction(language, name, methPtr, type, flags);
106106
assert func != null;
107-
PDecoratedMethod classMethod = PFactory.createClassmethodFromCallableObj(language, func);
107+
PDecoratedMethod classMethod = PFactory.createBuiltinClassmethodFromCallableObj(language, func);
108108
WriteAttributeToPythonObjectNode.executeUncached(classMethod, T___NAME__, name);
109109
WriteAttributeToPythonObjectNode.executeUncached(classMethod, T___DOC__, doc);
110110
HiddenAttr.WriteLongNode.executeUncached(classMethod, METHOD_DEF_PTR, methodDefPtr);

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextTypeBuiltins.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ private static PythonBuiltinObject typeAddMethod(PythonLanguage language, long m
298298
throw PRaiseNode.raiseStatic(EncapsulatingNodeReference.getCurrent().get(), PythonBuiltinClassType.ValueError, ErrorMessages.METHOD_CANNOT_BE_BOTH_CLASS_AND_STATIC);
299299
}
300300
assert func != null;
301-
return PFactory.createClassmethodFromCallableObj(language, func);
301+
return PFactory.createBuiltinClassmethodFromCallableObj(language, func);
302302
} else if (CExtContext.isMethStatic(flags)) {
303303
return PFactory.createStaticmethodFromCallableObj(language, func);
304304
}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/method/BuiltinClassmethodBuiltins.java

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* The Universal Permissive License (UPL), Version 1.0
@@ -61,21 +61,35 @@
6161
import com.oracle.graal.python.builtins.PythonBuiltinClassType;
6262
import com.oracle.graal.python.builtins.PythonBuiltins;
6363
import com.oracle.graal.python.builtins.objects.PNone;
64+
import com.oracle.graal.python.builtins.objects.function.PBuiltinFunction;
6465
import com.oracle.graal.python.builtins.objects.str.StringUtils.SimpleTruffleStringFormatNode;
6566
import com.oracle.graal.python.builtins.objects.type.TpSlots;
6667
import com.oracle.graal.python.builtins.objects.type.TypeNodes;
68+
import com.oracle.graal.python.builtins.objects.type.slots.TpSlotDescrGet.DescrGetBuiltinNode;
6769
import com.oracle.graal.python.lib.PyObjectGetAttr;
6870
import com.oracle.graal.python.lib.PyObjectLookupAttr;
6971
import com.oracle.graal.python.lib.PyObjectStrAsTruffleStringNode;
72+
import com.oracle.graal.python.nodes.ErrorMessages;
73+
import com.oracle.graal.python.nodes.PGuards;
74+
import com.oracle.graal.python.nodes.PRaiseNode;
75+
import com.oracle.graal.python.nodes.classes.IsSubtypeNode;
7076
import com.oracle.graal.python.nodes.function.PythonBuiltinBaseNode;
7177
import com.oracle.graal.python.nodes.function.builtins.PythonUnaryBuiltinNode;
78+
import com.oracle.graal.python.nodes.object.GetClassNode;
79+
import com.oracle.graal.python.runtime.exception.PException;
80+
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
81+
import com.oracle.truffle.api.HostCompilerDirectives.InliningCutoff;
7282
import com.oracle.truffle.api.dsl.Bind;
7383
import com.oracle.truffle.api.dsl.Cached;
84+
import com.oracle.truffle.api.dsl.Cached.Exclusive;
7485
import com.oracle.truffle.api.dsl.GenerateNodeFactory;
86+
import com.oracle.truffle.api.dsl.GenerateUncached;
7587
import com.oracle.truffle.api.dsl.NodeFactory;
88+
import com.oracle.truffle.api.dsl.ReportPolymorphism;
7689
import com.oracle.truffle.api.dsl.Specialization;
7790
import com.oracle.truffle.api.frame.VirtualFrame;
7891
import com.oracle.truffle.api.nodes.Node;
92+
import com.oracle.truffle.api.profiles.InlinedBranchProfile;
7993
import com.oracle.truffle.api.strings.TruffleString;
8094

8195
@CoreFunctions(extendClasses = PythonBuiltinClassType.PBuiltinClassMethod)
@@ -88,6 +102,97 @@ protected List<? extends NodeFactory<? extends PythonBuiltinBaseNode>> getNodeFa
88102
return BuiltinClassmethodBuiltinsFactory.getFactories();
89103
}
90104

105+
@Slot(SlotKind.tp_descr_get)
106+
@ReportPolymorphism
107+
@GenerateUncached
108+
@GenerateNodeFactory
109+
abstract static class GetNode extends DescrGetBuiltinNode {
110+
// If self.getCallable() is null, let the next @Specialization handle that
111+
@Specialization(guards = {"isSingleContext()", "isNoValue(type) == typeIsNoValue", "cachedSelf == self", "cachedCallable != null"}, limit = "3")
112+
static Object getCached(@SuppressWarnings("unused") PDecoratedMethod self, Object obj, Object type,
113+
@Bind Node inliningTarget,
114+
@SuppressWarnings("unused") @Cached(value = "self", weak = true) PDecoratedMethod cachedSelf,
115+
@Cached(value = "self.getCallable()", weak = true) Object cachedCallable,
116+
@Cached("isNoValue(type)") boolean typeIsNoValue,
117+
@Exclusive @Cached GetClassNode getClass,
118+
@Exclusive @Cached IsSubtypeNode isSubtypeNode,
119+
@Exclusive @Cached InlinedBranchProfile errorProfile,
120+
@Exclusive @Cached ClassmethodCommonBuiltins.MakeMethodNode makeMethod) {
121+
Object actualType = getType(inliningTarget, errorProfile, getClass, cachedCallable, typeIsNoValue, obj, type);
122+
return doGet(inliningTarget, isSubtypeNode, errorProfile, makeMethod, actualType, cachedCallable);
123+
}
124+
125+
@InliningCutoff
126+
@Specialization(replaces = "getCached")
127+
static Object get(PDecoratedMethod self, Object obj, Object type,
128+
@Bind Node inliningTarget,
129+
@Exclusive @Cached GetClassNode getClass,
130+
@Exclusive @Cached IsSubtypeNode isSubtypeNode,
131+
@Exclusive @Cached InlinedBranchProfile errorProfile,
132+
@Exclusive @Cached ClassmethodCommonBuiltins.MakeMethodNode makeMethod,
133+
@Exclusive @Cached PRaiseNode raiseNode) {
134+
Object callable = ClassmethodCommonBuiltins.getCallable(inliningTarget, self, raiseNode);
135+
Object actualType = getType(inliningTarget, errorProfile, getClass, callable, PGuards.isNoValue(type), obj, type);
136+
return doGet(inliningTarget, isSubtypeNode, errorProfile, makeMethod, actualType, callable);
137+
}
138+
139+
private static Object getType(Node inliningTarget, InlinedBranchProfile errorProfile, GetClassNode getClassNode,
140+
Object callable, boolean typeIsNoValue, Object obj, Object type) {
141+
if (typeIsNoValue) {
142+
if (PGuards.isNoValue(obj)) {
143+
errorProfile.enter(inliningTarget);
144+
throw raiseNeedsEitherObjOrType(inliningTarget, callable);
145+
}
146+
return getClassNode.execute(inliningTarget, obj);
147+
}
148+
return type;
149+
}
150+
151+
@TruffleBoundary
152+
private static PException raiseNeedsEitherObjOrType(Node inliningTarget, Object callable) {
153+
if (callable instanceof PBuiltinFunction pbf) {
154+
throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.TypeError,
155+
ErrorMessages.DESCRIPTOR_S_FOR_TYPE_S_NEEDS_EITHER_OBJ_OR_TYPE, pbf.getName(),
156+
TypeNodes.GetNameNode.executeUncached(pbf.getEnclosingType()));
157+
} else {
158+
throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.TypeError);
159+
}
160+
}
161+
162+
private static Object doGet(Node inliningTarget, IsSubtypeNode isSubtypeNode, InlinedBranchProfile errorProfile,
163+
ClassmethodCommonBuiltins.MakeMethodNode makeMethod, Object type, Object callable) {
164+
if (!PGuards.isPythonClass(type)) {
165+
errorProfile.enter(inliningTarget);
166+
throw raiseNeedsType(inliningTarget, callable, type);
167+
}
168+
// Not clear if we can get any other callable than PBuiltinFunction...
169+
if (callable instanceof PBuiltinFunction builtinFunction) {
170+
Object descriptorType = builtinFunction.getEnclosingType();
171+
if (!isSubtypeNode.execute(type, descriptorType)) {
172+
errorProfile.enter(inliningTarget);
173+
throw raiseRequiresSubtype(inliningTarget, builtinFunction, descriptorType, type);
174+
}
175+
}
176+
return makeMethod.execute(inliningTarget, type, callable);
177+
}
178+
179+
@TruffleBoundary
180+
private static RuntimeException raiseNeedsType(Node inliningTarget, Object callable, Object type) {
181+
if (callable instanceof PBuiltinFunction pbf) {
182+
throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.TypeError, ErrorMessages.DESCRIPTOR_S_FOR_TYPE_S_NEEDS_TYPE_NOT_P_AS_ARG_2,
183+
pbf.getName(), TypeNodes.GetNameNode.executeUncached(pbf.getEnclosingType()), type);
184+
} else {
185+
throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.TypeError);
186+
}
187+
}
188+
189+
@TruffleBoundary
190+
private static RuntimeException raiseRequiresSubtype(Node inliningTarget, PBuiltinFunction builtinFunction, Object descriptorType, Object type) {
191+
throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.TypeError, ErrorMessages.DESCRIPTOR_S_REQUIRES_SUBTYPE_OF_S_BUT_RECEIVED_S,
192+
builtinFunction.getName(), TypeNodes.GetNameNode.executeUncached(descriptorType), TypeNodes.GetNameNode.executeUncached(type));
193+
}
194+
}
195+
91196
@Builtin(name = J___NAME__, maxNumOfPositionalArgs = 1, isGetter = true)
92197
@GenerateNodeFactory
93198
abstract static class NameNode extends PythonUnaryBuiltinNode {

0 commit comments

Comments
 (0)