Skip to content

Commit 58aff7f

Browse files
committed
[GR-69732] More natural Java subclassing in Python. Github #470.
PullRequest: graalpython/3650
2 parents 31598df + 2c3046e commit 58aff7f

File tree

12 files changed

+417
-17
lines changed

12 files changed

+417
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ language runtime. The main focus is on user-observable behavior of the engine.
88
* Allocation reporting via Truffle has been removed. Python object sizes were never reported correctly, so the data was misleading and there was a non-neglible overhead for object allocations even when reporting was inactive.
99
* Better `readline` support via JLine. Autocompletion and history now works in `pdb`
1010
* Remove the intrinsified _ctypes module in favor of the native CPython version. This makes GraalPy's ctypes implementation more compatible and reduces the memory footprint of using ctypes.
11+
* Add a new, more natural style of subclassing Java classes from Python by passing the `new_style=True` keyword. Multiple levels of inheritance are supported, and `super()` calls both in the constructor override via `__new__` as well as in Java method overrides work as expected.
1112

1213
## Version 25.0.1
1314
* Allow users to keep going on unsupported JDK/OS/ARCH combinations at their own risk by opting out of early failure using `-Dtruffle.UseFallbackRuntime=true`, `-Dpolyglot.engine.userResourceCache=/set/to/a/writeable/dir`, `-Dpolyglot.engine.allowUnsupportedPlatform=true`, and `-Dpolyglot.python.UnsupportedPlatformEmulates=[linux|macos|windows]` and `-Dorg.graalvm.python.resources.exclude=native.files`.

docs/user/Native-Extensions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,6 @@ The implementation also relies on `venv` to work, even if you are not using exte
5050
To support creating multiple GraalPy contexts that access native modules within the same JVM or Native Image, we need to isolate them from each other.
5151
The current strategy for this is to copy the libraries and modify them such that the dynamic library loader of the operating system will isolate them for us.
5252
To do this, all GraalPy contexts in the same process (not just those in the same engine!) must set the `python.IsolateNativeModules` option to `true`.
53+
You should test your applications thoroughly if you want to use this feature, as there are many possiblities for native code to sidestep the library isolation through other process-wide global state.
5354

5455
For more details on this, see [our implementation details](https://github.com/oracle/graalpython/blob/master/docs/contributor/IMPLEMENTATION_DETAILS.md#c-extension-copying).

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,13 @@ def test_super(self):
720720
super(list, l).remove(0) # ArrayList#remove(int index)
721721
assert l == [6]
722722

723+
def test_issubclass_isinstance(self):
724+
from java.util import ArrayList, List
725+
assert issubclass(ArrayList, List)
726+
assert issubclass(ArrayList, ArrayList)
727+
assert isinstance(ArrayList(), List)
728+
assert isinstance(ArrayList(), ArrayList)
729+
723730
def test_java_array(self):
724731
import java
725732
il = java.type("int[]")(20)
@@ -1409,6 +1416,68 @@ def test_foreign_repl(self):
14091416
assert repr(Integer).startswith('<JavaClass[java.lang.Integer] at')
14101417
assert repr(i) == '22'
14111418

1419+
def test_natural_subclassing(self):
1420+
from java.util.logging import Level
1421+
1422+
class PythonLevel(Level, new_style=True):
1423+
def __new__(cls, name="default name", level=2):
1424+
return super().__new__(cls, name, level)
1425+
1426+
def __init__(self, *args, **kwarg):
1427+
self.misc_value = 42
1428+
1429+
def getName(self):
1430+
return super().getName() + " from Python with super()"
1431+
1432+
def pythonName(self):
1433+
return f"PythonName for Level {self.intValue()} named {super().getName()}"
1434+
1435+
def callStaticFromPython(self, name):
1436+
return self.parse(name)
1437+
1438+
pl = PythonLevel()
1439+
assert issubclass(PythonLevel, Level)
1440+
assert issubclass(PythonLevel, PythonLevel)
1441+
assert isinstance(pl, PythonLevel)
1442+
assert isinstance(pl, Level)
1443+
assert pl.getName() == "default name from Python with super()"
1444+
assert pl.intValue() == 2
1445+
assert pl.misc_value == 42
1446+
del pl.misc_value
1447+
try:
1448+
pl.misc_value
1449+
except AttributeError:
1450+
pass
1451+
else:
1452+
assert False
1453+
pl.misc_value = 43
1454+
assert pl.misc_value == 43
1455+
assert pl.pythonName() == "PythonName for Level 2 named default name"
1456+
assert pl.callStaticFromPython("INFO").getName() == "INFO"
1457+
assert PythonLevel.parse("INFO").getName() == "INFO"
1458+
1459+
class PythonLevel2(PythonLevel):
1460+
def __new__(cls):
1461+
return super().__new__(cls, "deeper name")
1462+
1463+
def pythonName(self):
1464+
return super().pythonName() + " from subclass"
1465+
1466+
def getName(self):
1467+
return super().getName() + " from subclass"
1468+
1469+
1470+
pl = PythonLevel2()
1471+
assert issubclass(PythonLevel2, Level)
1472+
assert issubclass(PythonLevel2, PythonLevel2)
1473+
assert isinstance(pl, PythonLevel2)
1474+
assert isinstance(pl, Level)
1475+
assert pl.getName() == "deeper name from Python with super() from subclass"
1476+
assert pl.pythonName() == "PythonName for Level 2 named deeper name from subclass"
1477+
assert pl.callStaticFromPython("INFO").getName() == "INFO"
1478+
assert PythonLevel2.parse("INFO").getName() == "INFO"
1479+
1480+
14121481
def test_jython_star_import(self):
14131482
if __graalpython__.jython_emulation_enabled:
14141483
g = {}

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/BuiltinFunctions.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2434,22 +2434,22 @@ public abstract static class BuildClassNode extends PythonVarargsBuiltinNode {
24342434

24352435
// No BoundaryCallContext: calls only internal well-behaved Python code
24362436
@TruffleBoundary
2437-
private static Object buildJavaClass(Object namespace, TruffleString name, Object base) {
2437+
private static Object buildJavaClass(Object namespace, TruffleString name, Object base, PKeyword[] keywords) {
24382438
// uncached PythonContext get, since this code path is slow in any case
24392439
Object module = PythonContext.get(null).lookupBuiltinModule(T___GRAALPYTHON__);
24402440
Object buildFunction = PyObjectLookupAttr.executeUncached(module, T_BUILD_JAVA_CLASS);
2441-
return CallNode.executeUncached(buildFunction, namespace, name, base);
2441+
return CallNode.executeUncached(buildFunction, new Object[]{namespace, name, base}, keywords);
24422442
}
24432443

24442444
@InliningCutoff
24452445
private static Object buildJavaClass(VirtualFrame frame, Node inliningTarget, PythonLanguage language, PFunction function, Object[] arguments,
2446-
CallDispatchers.FunctionCachedInvokeNode invokeBody,
2446+
PKeyword[] keywords, CallDispatchers.FunctionCachedInvokeNode invokeBody,
24472447
TruffleString name) {
24482448
PDict ns = PFactory.createDict(language, new DynamicObjectStorage(language));
24492449
Object[] args = PArguments.create(0);
24502450
PArguments.setSpecialArgument(args, ns);
24512451
invokeBody.execute(frame, inliningTarget, function, args);
2452-
return buildJavaClass(ns, name, arguments[1]);
2452+
return buildJavaClass(ns, name, arguments[1], keywords);
24532453
}
24542454

24552455
@Specialization
@@ -2492,7 +2492,7 @@ protected Object doItNonFunction(VirtualFrame frame, Object function, Object[] a
24922492

24932493
if (arguments.length == 2 && env.isHostObject(arguments[1]) && env.asHostObject(arguments[1]) instanceof Class<?>) {
24942494
// we want to subclass a Java class
2495-
return buildJavaClass(frame, inliningTarget, language, (PFunction) function, arguments, invokeBody, name);
2495+
return buildJavaClass(frame, inliningTarget, language, (PFunction) function, arguments, keywords, invokeBody, name);
24962496
}
24972497

24982498
class InitializeBuildClass {

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/foreign/ForeignAbstractClassBuiltins.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,25 @@
3636
import com.oracle.graal.python.builtins.CoreFunctions;
3737
import com.oracle.graal.python.builtins.PythonBuiltinClassType;
3838
import com.oracle.graal.python.builtins.PythonBuiltins;
39+
import com.oracle.graal.python.builtins.objects.common.SequenceStorageNodes;
40+
import com.oracle.graal.python.lib.PyObjectGetIter;
3941
import com.oracle.graal.python.nodes.function.PythonBuiltinBaseNode;
4042
import com.oracle.graal.python.nodes.function.builtins.PythonBinaryBuiltinNode;
4143
import com.oracle.graal.python.nodes.function.builtins.PythonUnaryBuiltinNode;
4244
import com.oracle.graal.python.runtime.GilNode;
4345
import com.oracle.graal.python.runtime.object.PFactory;
46+
import com.oracle.graal.python.runtime.sequence.storage.SequenceStorage;
4447
import com.oracle.truffle.api.CompilerDirectives;
4548
import com.oracle.truffle.api.dsl.Bind;
4649
import com.oracle.truffle.api.dsl.Cached;
4750
import com.oracle.truffle.api.dsl.GenerateNodeFactory;
4851
import com.oracle.truffle.api.dsl.NodeFactory;
4952
import com.oracle.truffle.api.dsl.Specialization;
53+
import com.oracle.truffle.api.frame.VirtualFrame;
5054
import com.oracle.truffle.api.interop.InteropLibrary;
5155
import com.oracle.truffle.api.interop.UnsupportedMessageException;
5256
import com.oracle.truffle.api.library.CachedLibrary;
57+
import com.oracle.truffle.api.nodes.Node;
5358

5459
/*
5560
* NOTE: We are not using IndirectCallContext here in this file
@@ -67,10 +72,25 @@ protected List<? extends NodeFactory<? extends PythonBuiltinBaseNode>> getNodeFa
6772
@Builtin(name = J___BASES__, minNumOfPositionalArgs = 1, isGetter = true, isSetter = false)
6873
@GenerateNodeFactory
6974
abstract static class BasesNode extends PythonUnaryBuiltinNode {
70-
@Specialization
71-
static Object getBases(Object self,
75+
@Specialization(limit = "2")
76+
static Object getBases(VirtualFrame frame, Object self,
77+
@Bind Node inliningTarget,
78+
@CachedLibrary("self") InteropLibrary lib,
79+
@Cached PyObjectGetIter getIter,
80+
@Cached SequenceStorageNodes.CreateStorageFromIteratorNode createStorageFromIteratorNode,
7281
@Bind PythonLanguage language) {
73-
return PFactory.createEmptyTuple(language);
82+
if (lib.hasMetaParents(self)) {
83+
try {
84+
Object parents = lib.getMetaParents(self);
85+
Object iterObj = getIter.execute(frame, inliningTarget, parents);
86+
SequenceStorage storage = createStorageFromIteratorNode.execute(frame, iterObj);
87+
return PFactory.createTuple(language, storage);
88+
} catch (UnsupportedMessageException e) {
89+
throw CompilerDirectives.shouldNotReachHere(e);
90+
}
91+
} else {
92+
return PFactory.createEmptyTuple(language);
93+
}
7494
}
7595
}
7696

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/type/TypeNodes.java

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@
222222
import com.oracle.truffle.api.CompilerDirectives.ValueType;
223223
import com.oracle.truffle.api.HostCompilerDirectives.InliningCutoff;
224224
import com.oracle.truffle.api.RootCallTarget;
225+
import com.oracle.truffle.api.TruffleLanguage.Env;
225226
import com.oracle.truffle.api.dsl.Bind;
226227
import com.oracle.truffle.api.dsl.Cached;
227228
import com.oracle.truffle.api.dsl.Cached.Exclusive;
@@ -237,6 +238,8 @@
237238
import com.oracle.truffle.api.frame.Frame;
238239
import com.oracle.truffle.api.frame.VirtualFrame;
239240
import com.oracle.truffle.api.interop.InteropLibrary;
241+
import com.oracle.truffle.api.interop.UnknownIdentifierException;
242+
import com.oracle.truffle.api.interop.UnsupportedMessageException;
240243
import com.oracle.truffle.api.library.CachedLibrary;
241244
import com.oracle.truffle.api.nodes.ControlFlowException;
242245
import com.oracle.truffle.api.nodes.Node;
@@ -1482,23 +1485,23 @@ static boolean doManaged(PythonManagedClass left, PythonManagedClass right) {
14821485
}
14831486

14841487
@Specialization
1485-
static boolean doManaged(PythonBuiltinClassType left, PythonBuiltinClassType right) {
1488+
static boolean doTypeType(PythonBuiltinClassType left, PythonBuiltinClassType right) {
14861489
return left == right;
14871490
}
14881491

14891492
@Specialization
1490-
static boolean doManaged(PythonBuiltinClassType left, PythonBuiltinClass right) {
1493+
static boolean doTypeClass(PythonBuiltinClassType left, PythonBuiltinClass right) {
14911494
return left == right.getType();
14921495
}
14931496

14941497
@Specialization
1495-
static boolean doManaged(PythonBuiltinClass left, PythonBuiltinClassType right) {
1498+
static boolean doClassType(PythonBuiltinClass left, PythonBuiltinClassType right) {
14961499
return left.getType() == right;
14971500
}
14981501

14991502
@Specialization
15001503
@InliningCutoff
1501-
static boolean doNativeSingleContext(PythonAbstractNativeObject left, PythonAbstractNativeObject right,
1504+
static boolean doNative(PythonAbstractNativeObject left, PythonAbstractNativeObject right,
15021505
@CachedLibrary(limit = "1") InteropLibrary lib) {
15031506
if (left == right) {
15041507
return true;
@@ -1509,6 +1512,44 @@ static boolean doNativeSingleContext(PythonAbstractNativeObject left, PythonAbst
15091512
return lib.isIdentical(left.getPtr(), right.getPtr(), lib);
15101513
}
15111514

1515+
@Specialization(guards = {"isForeignObject(left)", "isForeignObject(right)"})
1516+
@InliningCutoff
1517+
static boolean doOther(Object left, Object right,
1518+
@Bind PythonContext context,
1519+
@CachedLibrary(limit = "2") InteropLibrary lib) {
1520+
if (lib.isMetaObject(left) && lib.isMetaObject(right)) {
1521+
if (left == right) {
1522+
return true;
1523+
}
1524+
// *sigh*... Host classes have split personality with a "static" and a "class"
1525+
// side, and that affects identity comparisons. And they report their "class" sides
1526+
// as bases, but importing from Java gives you the "static" side.
1527+
Env env = context.getEnv();
1528+
if (env.isHostObject(left) && env.isHostObject(right)) {
1529+
// the activation of isMemberReadable and later readMember serves as branch
1530+
// profile
1531+
boolean leftIsStatic = lib.isMemberReadable(left, "class");
1532+
boolean rightIsStatic = lib.isMemberReadable(right, "class");
1533+
if (leftIsStatic != rightIsStatic) {
1534+
try {
1535+
if (leftIsStatic) {
1536+
left = lib.readMember(left, "class");
1537+
} else {
1538+
assert rightIsStatic;
1539+
right = lib.readMember(right, "class");
1540+
}
1541+
} catch (UnsupportedMessageException | UnknownIdentifierException e) {
1542+
throw CompilerDirectives.shouldNotReachHere(e);
1543+
}
1544+
}
1545+
}
1546+
if (lib.isIdentical(left, right, lib)) {
1547+
return true;
1548+
}
1549+
}
1550+
return false;
1551+
}
1552+
15121553
@Fallback
15131554
static boolean doOther(@SuppressWarnings("unused") Object left, @SuppressWarnings("unused") Object right) {
15141555
return false;

0 commit comments

Comments
 (0)