Skip to content

Commit 86885bf

Browse files
committed
[GR-58278] Add documentation for the interop registry
PullRequest: graalpython/3489
2 parents 6171b1f + 47b4fbc commit 86885bf

File tree

2 files changed

+170
-64
lines changed

2 files changed

+170
-64
lines changed

Diff for: docs/user/Interoperability.md

+167-64
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,174 @@ An example in this sense are the `numpy` numeric types (for example, `numpy.int3
249249
| register_interop_behavior | Takes the receiver **type** as first argument. The remainder keyword arguments correspond to the respective interop messages. Not All interop messages are supported. |
250250
| get_registered_interop_behavior | Takes the receiver **type** as first argument. Returns the list of extended interop messages for the given type. |
251251
| @interop_behavior | Class decorator, takes the receiver **type** as only argument. The interop messages are extended via **static** methods defined in the decorated class (supplier). |
252+
| register_interop_type | Takes a `foreign class` and `python class` as positional arguments and `allow_method_overwrites` as optional argument (default: `False`). Every instance of foreign class is then treated as an instance of the given python class. |
253+
| @interop_type | Class decorator, takes the `foreign class` and optionally `allow_method_overwrites` as arguments. The instances of foreign class will be treated as an instance of the annotated python class. |
252254

253-
#### Supported messages
255+
### Usage Examples
256+
257+
#### Interop Behavior
258+
259+
A simple `register_interop_behavior` API is available to register interop behaviors for existing types:
260+
261+
```python
262+
import polyglot
263+
import numpy
264+
265+
polyglot.register_interop_behavior(numpy.int32,
266+
is_number=True,
267+
fitsInByte=lambda v: -128 <= v < 128,
268+
fitsInShort=lambda v: -0x8000 <= v < 0x8000,
269+
fitsInInt = True,
270+
fitsInLong = True,
271+
fitsInBigInteger = True,
272+
asByte = int,
273+
asShort = int,
274+
asInt = int,
275+
asLong = int,
276+
asBigInteger = int,
277+
)
278+
```
279+
280+
The `@interop_behavior` decorator may be more convenient when declaring more behaviors.
281+
Interop message extension is achieved via **static** methods of the decorated class.
282+
The names of the static methods are identical to the keyword names expected by `register_interop_behavior`.
283+
284+
```python
285+
from polyglot import interop_behavior
286+
import numpy
287+
288+
289+
@interop_behavior(numpy.float64)
290+
class Int8InteropBehaviorSupplier:
291+
@staticmethod
292+
def is_number(_):
293+
return True
294+
295+
@staticmethod
296+
def fitsInDouble(_):
297+
return True
298+
299+
@staticmethod
300+
def asDouble(v):
301+
return float(v)
302+
```
303+
304+
Both classes can then behave as expected when embedded:
305+
306+
```java
307+
import java.nio.file.Files;
308+
import java.nio.file.Path;
309+
310+
import org.graalvm.polyglot.Context;
311+
312+
class Main {
313+
public static void main(String[] args) {
314+
try (var context = Context.create()) {
315+
context.eval("python", Files.readString(Path.of("path/to/interop/behavior/script.py")));
316+
assert context.eval("python", "numpy.float64(12)").asDouble() == 12.0;
317+
assert context.eval("python", "numpy.int32(12)").asByte() == 12;
318+
}
319+
}
320+
}
321+
```
322+
#### Interop Types
323+
The `register_interop_type` API allows the usage of python classes for foreign objects.
324+
The type of such a foreign object will no longer be `foreign`.
325+
Instead, it will be a generated class with the registered python classes and `foreign` and as super classes.
326+
This allows custom mapping of foreign methods and attributes to Python's magic methods or more idiomatic Python code.
327+
328+
```java
329+
package org.example;
330+
331+
class MyJavaClass {
332+
private int x;
333+
private int y;
334+
335+
public MyJavaClass(int x, int y) {
336+
this.x = x;
337+
this.y = y;
338+
}
339+
340+
public int getX() {
341+
return x;
342+
}
343+
344+
public int getY() {
345+
return y;
346+
}
347+
}
348+
```
349+
350+
```java
351+
import org.example.MyJavaClass;
352+
353+
class Main {
354+
355+
356+
public static void main(String[] args) {
357+
MyJavaClass myJavaObject = new MyJavaClass(42, 17);
358+
try (var context = Context.create()) {
359+
// myJavaObject will be globally available in example.py as my_java_object
360+
context.getBindings("python").putMember("my_java_object", myJavaObject);
361+
context.eval(Source.newBuilder("python", "example.py"));
362+
}
363+
}
364+
}
365+
```
366+
367+
```python
368+
# example.py
369+
import java
370+
from polyglot import register_interop_type
371+
372+
print(my_java_object.getX()) # 42
373+
print(type(my_java_object)) # <class 'foreign'>
374+
375+
class MyPythonClass:
376+
def get_tuple(self):
377+
return (self.getX(), self.getY())
378+
379+
foreign_class = java.type("org.example.MyJavaClass")
380+
381+
register_interop_type(foreign_class, MyPythonClass)
382+
383+
print(my_java_object.get_tuple()) # (42, 17)
384+
print(type(my_java_object)) # <class 'polyglot.Java_org.example.MyJavaClass_generated'>
385+
print(type(my_java_object).mro()) # [polyglot.Java_org.example.MyJavaClass_generated, MyPythonClass, foreign, object]
386+
387+
class MyPythonClassTwo:
388+
def get_tuple(self):
389+
return (self.getY(), self.getX())
390+
391+
def __str__(self):
392+
return f"MyJavaInstance(x={self.getX()}, y={self.getY()}"
393+
394+
# If 'allow_method_overwrites=True' is not given, this would lead to an error due to the method conflict of 'get_tuple'
395+
register_interop_type(foreign_class, MyPythonClassTwo, allow_method_overwrites=True)
396+
397+
# A newly registered class will be before already registered classes in the mro.
398+
# It allows overwriting methods from already registered classes with the flag 'allow_method_overwrites=True'
399+
print(type(my_java_object).mro()) # [generated_class, MyPythonClassTwo, MyPythonClass, foreign, object]
400+
401+
print(my_java_object.get_tuple()) # (17, 42)
402+
print(my_java_object) # MyJavaInstance(x=42, y=17)
403+
```
404+
405+
Registering classes may be more convenient with `@interop_type`:
406+
```python
407+
import java
408+
from polyglot import interop_type
409+
410+
411+
foreign_class = java.type("org.example.MyJavaClass")
412+
413+
@interop_type(foreign_class)
414+
class MyPythonClass:
415+
def get_tuple(self):
416+
return (self.getX(), self.getY())
417+
```
418+
419+
### Supported messages
254420
255421
The majority (with some exceptions) of the interop messages are supported by the interop behavior extension API, as shown in the table below.
256422
The naming convention for the `register_interop_behavior` keyword arguments follows the _snake_case_ naming convention, i.e. the interop `fitsInLong` message
@@ -314,66 +480,3 @@ The table below describes the supported interop messages:
314480
| readHashValue | read_hash_value | object |
315481
| writeHashEntry | write_hash_entry | NoneType |
316482
| removeHashEntry | remove_hash_entry | NoneType |
317-
318-
### Usage Example
319-
320-
A simple `register_interop_behavior` API is available to register interop behaviors for existing types:
321-
322-
```python
323-
import polyglot
324-
import numpy
325-
326-
polyglot.register_interop_behavior(numpy.int32,
327-
is_number=True,
328-
fitsInByte=lambda v: -128 <= v < 128,
329-
fitsInShort=lambda v: -0x8000 <= v < 0x8000
330-
fitsInInt=True,
331-
fitsInLong=True,
332-
fitsInBigInteger=True,
333-
asByte=int,
334-
asShort=int,
335-
asInt=int,
336-
asLong=int,
337-
asBigInteger=int,
338-
)
339-
```
340-
341-
The `@interop_behavior` decorator may be more convenient when declaring more behaviors.
342-
Interop message extension is achieved via **static** methods of the decorated class.
343-
The names of the static methods are identical to the keyword names expected by `register_interop_behavior`.
344-
345-
```python
346-
from polyglot import interop_behavior
347-
import numpy
348-
349-
@interop_behavior(numpy.float64)
350-
class Int8InteropBehaviorSupplier:
351-
@staticmethod
352-
def is_number(_):
353-
return True
354-
355-
@staticmethod
356-
def fitsInDouble(_):
357-
return True
358-
359-
@staticmethod
360-
def asDouble(v):
361-
return float(v)
362-
```
363-
364-
Both classes can then behave as expected when embedded:
365-
```java
366-
import java.nio.file.Files;
367-
import java.nio.file.Path;
368-
import org.graalvm.polyglot.Context;
369-
370-
class Main {
371-
public static void main(String[] args) {
372-
try (var context = Context.create()) {
373-
context.eval("python", Files.readString(Path.of("path/to/interop/behavior/script.py")));
374-
assert context.eval("python", "numpy.float64(12)").asDouble() == 12.0;
375-
assert context.eval("python", "numpy.int32(12)").asByte() == 12;
376-
}
377-
}
378-
}
379-
```

Diff for: graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/object/GetRegisteredClassNode.java

+3
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ static Object lookup(Node inliningTarget,
138138
ObjectHashMap.GetNode getNode) {
139139
try {
140140
// lookup generated classes
141+
// For now, we assume that the keys in the InteropGeneratedClassCache and
142+
// InteropTypeRegistry do not call back to any python code.
143+
// Otherwise, the pattern listed in IndirectCallContext#enter must be used.
141144
var possiblePythonClass = getNode.execute(null,
142145
inliningTarget,
143146
PythonContext.get(inliningTarget).interopGeneratedClassCache,

0 commit comments

Comments
 (0)