oracle / graalpython

GraalPy – A high-performance embeddable Python 3 runtime for Java
https://www.graalvm.org/python/
Other
1.25k stars 110 forks source link

Make Proxy/foreign objects operate like their Python equivalents #248

Open aeriksson opened 2 years ago

aeriksson commented 2 years ago

As it stands, the usefulness of Proxy objects as passed to Python is somewhat limited. They all map to the foreign class, and AFAICT there's often no way to tell them apart short of randomly attempting to call methods that are known to behave differently for different Proxy classes and seeing if/which exceptions are thrown.

I'm passing 'simple' data structures (i.e. ones that can be JSON-serialized) to Python from Java as Proxy objects, and I'd like these to behave like 'normal' Python objects (ideally they should be indistinguishable from what I'd get by JSON-serializing the data in Java and calling json.loads() on it in Python). To get this behavior currently, I have to manually eval some Python translation code (which some iffy exception-based 'type checks' as outlined above) to replace the foreign values with proper Python equivalents. Since this has to happen both for bindings and function (ProxyExecutable) return values, it ends up complicating my code quite a bit, as well as slowing things down.

I wouldn't have to do any of this work if the Proxy types acted as their corresponding Python builtins by default. It seems to me like:

Since Python is duck-typed, it should be relatively straightforward to make these types act as their Python equivalents by simply implementing the relevant attributes.

Looking at the built-in attributes of the aforementioned Python types, we see (Python 3.9.7):

> dir({})
['__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__ior__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__ror__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']

> dir([])
['__add__', '__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

> class Foo(): pass
> dir(Foo())
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

For the Proxy types, dir() returns nothing (except for ProxyObject, where it returns the provided methods but none of the __ attributes). (It seems like some of these methods are still implemented (so __dir__ isn't working quite right) — for example __str__ works as expected for ProxyArray, and it exists for ProxyHashMap but doesn't print the actual map.)

Implementing (most of) these functions on the relevant proxy objects doesn't seem like it'd be that difficult, and would go a long way towards making graalpython more user-friendly!

P.S. thanks for this awesome project!

eregon commented 2 years ago

I think something like https://2022.ecoop.org/details/truffle-2022/2/Designing-an-intuitive-language-agnostic-integration-of-foreign-objects-in-Ruby in graalpython would help for this issue.