Skip to content

Commit 8d88d07

Browse files
committed
Adapt new_style Java subclassing code to support super() and multiple levels of Python subclasses under Java
1 parent f159661 commit 8d88d07

File tree

3 files changed

+148
-78
lines changed

3 files changed

+148
-78
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. 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`.

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,6 +1416,68 @@ def test_foreign_repl(self):
14161416
assert repr(Integer).startswith('<JavaClass[java.lang.Integer] at')
14171417
assert repr(i) == '22'
14181418

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):
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+
14191481
def test_jython_star_import(self):
14201482
if __graalpython__.jython_emulation_enabled:
14211483
g = {}

graalpython/lib-graalpython/__graalpython__.py

Lines changed: 85 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -170,81 +170,42 @@ def factory (cls, *args):
170170
return resultClass
171171

172172

173+
_CUSTOM_JAVA_SUBCLASS_BACKSTOPS = {}
174+
175+
173176
@builtin
174177
def build_new_style_java_class(module, ns, name, base):
175178
import polyglot
179+
import types
176180

177-
# First, generate the Java subclass using the Truffle API. Instances of
178-
# this class is what we want to generate.
179181
JavaClass = __graalpython__.extend(base)
182+
if JavaClass not in _CUSTOM_JAVA_SUBCLASS_BACKSTOPS:
183+
class MroClass:
184+
def __getattr__(self, name):
185+
sentinel = object()
186+
# An attribute access on the Java instance failed, check the
187+
# delegate and then the static Java members
188+
result = getattr(self.this, name, sentinel)
189+
if result is sentinel:
190+
return getattr(self.getClass().static, name)
191+
else:
192+
return result
193+
194+
def __setattr__(self, name, value):
195+
# An attribute access on the Java instance failed, use the delegate
196+
setattr(self.this, name, value)
180197

181-
# Second, generate the delegate object class. Code calling from Java will
182-
# end up delegating methods to an instance of this type and the Java object
183-
# will use this delegate instance to manage dynamic attributes.
184-
#
185-
# The __init__ function would not do what the user thinks, so we take it
186-
# out and call it explicitly in the factory below. The `self` passed into
187-
# those Python-defined methods is the delegate instance, but that would be
188-
# confusing for users. So we wrap all methods to get to the Java instance
189-
# and pass that one as `self`.
190-
delegate_namespace = dict(**ns)
191-
delegate_namespace["__java_init__"] = delegate_namespace.pop("__init__", lambda self, *a, **kw: None)
192-
193-
def python_to_java_decorator(fun):
194-
return lambda self, *args, **kwds: fun(self.__this__, *args, **kwds)
195-
196-
for n, v in delegate_namespace.items():
197-
if type(v) == type(python_to_java_decorator):
198-
delegate_namespace[n] = python_to_java_decorator(v)
199-
DelegateClass = type(f"PythonDelegateClassFor{base}", (object,), delegate_namespace)
200-
DelegateClass.__qualname__ = DelegateClass.__name__
201-
202-
# Third, generate the class used to inject into the MRO of the generated
203-
# Java subclass. Code calling from Python will go through this class for
204-
# lookup.
205-
#
206-
# The `self` passed into those Python-defined methods will be the Java
207-
# instance. We add `__getattr__`, `__setattr__`, and `__delattr__`
208-
# implementations to look to the Python delegate object when the Java-side
209-
# lookup fails. For convenience, we also allow retrieving static fields
210-
# from Java.
211-
mro_namespace = dict(**ns)
212-
213-
def java_getattr(self, name):
214-
if name == "super":
215-
return __graalpython__.super(self)
216-
sentinel = object()
217-
result = getattr(self.this, name, sentinel)
218-
if result is sentinel:
219-
return getattr(self.getClass().static, name)
220-
else:
221-
return result
222-
223-
mro_namespace['__getattr__'] = java_getattr
224-
mro_namespace['__setattr__'] = lambda self, name, value: setattr(self.this, name, value)
225-
mro_namespace['__delattr__'] = lambda self, name: delattr(self.this, name)
226-
227-
@classmethod
228-
def factory(cls, *args, **kwds):
229-
# create the delegate object
230-
delegate = DelegateClass()
231-
# create the Java object (remove the class argument and add the delegate instance)
232-
java_object = polyglot.__new__(JavaClass, *(args[1:] + (delegate, )))
233-
delegate.__this__ = java_object
234-
# call the __init__ function on the delegate object now that the Java instance is available
235-
delegate.__java_init__(*args[1:], **kwds)
236-
return java_object
237-
238-
mro_namespace['__constructor__'] = factory
239-
if '__new__' not in mro_namespace:
240-
mro_namespace['__new__'] = classmethod(lambda cls, *args, **kwds: cls.__constructor__(*args, **kwds))
241-
MroClass = type(f"PythonMROMixinFor{base}", (object,), mro_namespace)
242-
MroClass.__qualname__ = MroClass.__name__
243-
polyglot.register_interop_type(JavaClass, MroClass)
244-
245-
# Finally, generate a factory that implements the factory and type checking
246-
# methods and denies inheriting again
247-
class FactoryMeta(type):
198+
def __delattr__(self, name):
199+
# An attribute access on the Java instance failed, use the delegate
200+
delattr(self.this, name)
201+
202+
# This may race, so we allow_method_overwrites, at the only danger to
203+
# insert a few useless classes into the MRO
204+
polyglot.register_interop_type(JavaClass, MroClass, allow_method_overwrites=True)
205+
206+
# A class to make sure that the returned Python class can be used for
207+
# issubclass and isinstance checks with the Java instances
208+
class JavaSubclassMeta(type):
248209
@property
249210
def __bases__(self):
250211
return (JavaClass,)
@@ -257,16 +218,62 @@ def __subclasscheck__(cls, derived):
257218

258219
def __new__(mcls, name, bases, namespace):
259220
if bases:
260-
raise NotImplementedError("Grandchildren of Java classes are not supported")
221+
new_class = None
222+
223+
class custom_super():
224+
def __init__(self, start_type=None, object_or_type=None):
225+
assert start_type is None and object_or_type is None, "super() calls in Python class inheriting from Java must not receive arguments"
226+
f = sys._getframe(1)
227+
self.self = f.f_locals[f.f_code.co_varnames[0]]
228+
229+
def __getattribute__(self, name):
230+
if name == "__class__":
231+
return __class__
232+
if name == "self":
233+
return object.__getattribute__(self, "self")
234+
for t in new_class.mro()[1:]:
235+
if t == DelegateSuperclass:
236+
break
237+
if name in t.__dict__:
238+
value = t.__dict__[name]
239+
if get := getattr(value, "__get__", None):
240+
return get(self.self.this)
241+
return value
242+
return getattr(__graalpython__.super(self.self), name)
243+
244+
# Wrap all methods so that the `self` inside is always a Java object, and
245+
# adapt the globals in the functions to provide a custom super() if
246+
# necessary
247+
def self_as_java_wrapper(k, value):
248+
if type(value) is not types.FunctionType:
249+
return value
250+
if k in ("__new__", "__class_getitem__"):
251+
return value
252+
if "super" in value.__code__.co_names:
253+
value = types.FunctionType(
254+
value.__code__,
255+
value.__globals__ | {"super": custom_super},
256+
name=value.__name__,
257+
argdefs=value.__defaults__,
258+
closure=value.__closure__,
259+
)
260+
return lambda self, *args, **kwds: value(self.__this__, *args, **kwds)
261+
namespace = {k: self_as_java_wrapper(k, v) for k, v in namespace.items()}
262+
new_class = type.__new__(mcls, name, bases, namespace)
263+
return new_class
261264
return type.__new__(mcls, name, bases, namespace)
262265

263-
class FactoryClass(metaclass=FactoryMeta):
264-
@classmethod
265-
def __new__(cls, *args, **kwds):
266-
return MroClass.__new__(*args, **kwds)
266+
def __getattr__(self, name):
267+
return getattr(JavaClass, name)
267268

268-
FactoryClass.__name__ = ns['__qualname__'].rsplit(".", 1)[-1]
269-
FactoryClass.__qualname__ = ns['__qualname__']
270-
FactoryClass.__module__ = ns['__module__']
269+
# A class that defines the required construction for the Java instances, so
270+
# the Python code can actually override __new__ to affect the construction
271+
# of the Java object
272+
class DelegateSuperclass(metaclass=JavaSubclassMeta):
273+
def __new__(cls, *args, **kwds):
274+
delegate = object.__new__(cls)
275+
java_object = polyglot.__new__(JavaClass, *(args + (delegate,)))
276+
delegate.__this__ = java_object
277+
return java_object
271278

272-
return FactoryClass
279+
return type(name, (DelegateSuperclass,), ns)

0 commit comments

Comments
 (0)