Skip to content

Commit 0440498

Browse files
committed
[GR-42711] Expose foreign types on the polyglot module, make ForeignBoolean inherit from ForeignNumber and complete ForeignNumber
PullRequest: graalpython/3544
2 parents d84733e + 68508a8 commit 0440498

File tree

19 files changed

+970
-456
lines changed

19 files changed

+970
-456
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ language runtime. The main focus is on user-observable behavior of the engine.
99
* When calling a method on a foreign object in Python code, Python methods are now prioritized over foreign members.
1010
* Added `polyglot.register_interop_type` and `@polyglot.interop_type` to define custom Python methods for a given foreign class/type. See [the documentation](https://github.com/oracle/graalpython/blob/master/docs/user/Interoperability.md#the-interoperability-extension-api) for more information.
1111
* Foreign objects are now given a Python class corresponding to their interop traits.
12-
* Foreign lists now inherit from Python `list`, foreign dictionaries from `dict`, foreign strings from `str`, foreign iterators from `iterator`, foreign exceptions from `BaseException`, foreign numbers from `ForeignNumberType` and foreign none/null from `NoneType`.
12+
* Foreign lists now inherit from Python `list`, foreign dictionaries from `dict`, foreign strings from `str`, foreign iterators from `iterator`, foreign exceptions from `BaseException`, foreign numbers from `polyglot.ForeignNumber`, foreign booleans from `polyglot.ForeignBoolean`, and foreign null values from `NoneType`.
1313
* This means all Python methods of these types are available on the corresponding foreign objects, which behave as close as possible as if they were Python objects.
1414
* See [the documentation](https://github.com/oracle/graalpython/blob/master/docs/user/Interoperability.md#interacting-with-foreign-objects-from-python-scripts) for more information.
1515
* Remove support for running with Sulong managed both in embeddings as well as through the `graalpy-managed` launcher.

docs/user/Interoperability.md

Lines changed: 45 additions & 41 deletions
Large diffs are not rendered by default.

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

Lines changed: 207 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@
3737
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3838
# SOFTWARE.
3939

40+
import math
4041
import os
42+
import types
4143
import unittest
4244
from unittest import skipIf, skipUnless
4345

@@ -106,6 +108,72 @@ class PyString(str):
106108

107109
@skipUnless(sys.implementation.name == "graalpy" and not __graalpython__.is_native, "interop")
108110
class InteropTests(unittest.TestCase):
111+
# This should run first, before other tests create foreign objects which creates more foreign classes.
112+
# We want to ensure all single-trait classes are always defined, so users can rely on them.
113+
def test_single_trait_classes(self):
114+
classes = [
115+
polyglot.ForeignObject,
116+
polyglot.ForeignList,
117+
polyglot.ForeignBoolean,
118+
polyglot.ForeignException,
119+
polyglot.ForeignExecutable,
120+
polyglot.ForeignDict,
121+
polyglot.ForeignInstantiable,
122+
polyglot.ForeignIterable,
123+
polyglot.ForeignIterator,
124+
polyglot.ForeignAbstractClass,
125+
polyglot.ForeignNone,
126+
polyglot.ForeignNumber,
127+
polyglot.ForeignString,
128+
]
129+
130+
for c in classes:
131+
self.assertIsInstance(c, type)
132+
if c is polyglot.ForeignBoolean:
133+
self.assertIs(c.__base__, polyglot.ForeignNumber)
134+
elif c is not polyglot.ForeignObject:
135+
self.assertIs(c.__base__, polyglot.ForeignObject)
136+
137+
def test_get_class(self):
138+
def wrap(obj):
139+
return __graalpython__.foreign_wrapper(obj)
140+
141+
def t(obj):
142+
return type(wrap(obj))
143+
144+
self.assertEqual(t(object()), polyglot.ForeignObject)
145+
self.assertEqual(t([]), polyglot.ForeignList)
146+
self.assertEqual(t(True), polyglot.ForeignBoolean)
147+
self.assertEqual(t(BaseException()), polyglot.ForeignException)
148+
self.assertEqual(t(lambda: None), polyglot.ForeignExecutable)
149+
self.assertEqual(t({}), polyglot.ForeignDict)
150+
# ForeignInstantiable
151+
self.assertEqual(t((e for e in [1])), polyglot.ForeignIteratorIterable)
152+
self.assertEqual(t(iter([1])), polyglot.ForeignIteratorIterable)
153+
self.assertEqual(t(object), polyglot.ForeignExecutableClass)
154+
self.assertEqual(t(None), polyglot.ForeignNone)
155+
self.assertEqual(t(1), polyglot.ForeignNumber)
156+
self.assertEqual(t("abc"), polyglot.ForeignString)
157+
158+
from java.lang import Object, Boolean, Integer, Throwable, Thread, Number, String
159+
from java.util import ArrayList, HashMap, ArrayDeque
160+
from java.math import BigInteger
161+
null = Integer.getInteger("something_that_does_not_exists")
162+
163+
self.assertEqual(type(Object()), polyglot.ForeignObject)
164+
self.assertEqual(type(ArrayList()), polyglot.ForeignList)
165+
self.assertEqual(type(wrap(Boolean.valueOf(True))), polyglot.ForeignBoolean)
166+
self.assertEqual(type(Throwable()), polyglot.ForeignException)
167+
self.assertEqual(type(Thread()), polyglot.ForeignExecutable) # Thread implements Runnable
168+
self.assertEqual(type(HashMap()), polyglot.ForeignDict)
169+
self.assertEqual(type(Object), polyglot.ForeignClass) # ForeignDictIterable
170+
self.assertEqual(type(ArrayDeque()), polyglot.ForeignIterable)
171+
self.assertEqual(type(ArrayList().iterator()), polyglot.ForeignIterator)
172+
self.assertEqual(type(Number), polyglot.ForeignAbstractClass)
173+
self.assertEqual(type(null), polyglot.ForeignNone)
174+
self.assertEqual(type(BigInteger.valueOf(42)), polyglot.ForeignNumber)
175+
self.assertEqual(type(wrap(String("abc"))), polyglot.ForeignString)
176+
109177
def test_import(self):
110178
def some_function():
111179
return "hello, polyglot world!"
@@ -403,9 +471,10 @@ def test_java_import_from_jar(self):
403471
os.unlink(tempname)
404472

405473
def test_java_class(self):
406-
from java.lang import Integer, NumberFormatException
407-
self.assertEqual(['ForeignInstantiableType', 'foreign', 'object'], [t.__name__ for t in type(Integer).mro()])
408-
self.assertEqual(['ForeignInstantiableType', 'foreign', 'object'], [t.__name__ for t in type(NumberFormatException).mro()])
474+
from java.lang import Integer, Number, NumberFormatException
475+
self.assertEqual(type(Integer).mro(), [polyglot.ForeignClass, polyglot.ForeignInstantiable, polyglot.ForeignAbstractClass, polyglot.ForeignObject, object])
476+
self.assertEqual(type(Number).mro(), [polyglot.ForeignAbstractClass, polyglot.ForeignObject, object])
477+
self.assertEqual(type(NumberFormatException).mro(), [polyglot.ForeignClass, polyglot.ForeignInstantiable, polyglot.ForeignAbstractClass, polyglot.ForeignObject, object])
409478

410479
def test_java_exceptions(self):
411480
# TODO: more tests
@@ -417,7 +486,7 @@ def test_java_exceptions(self):
417486

418487
assert isinstance(e, BaseException)
419488
assert BaseException in type(e).mro()
420-
self.assertEqual(['ForeignException', 'BaseException', 'foreign', 'object'], [t.__name__ for t in type(e).mro()])
489+
self.assertEqual([polyglot.ForeignException, BaseException, polyglot.ForeignObject, object], type(e).mro())
421490
self.assertEqual('java.lang.NumberFormatException: For input string: \"99\" under radix 8', str(e))
422491
self.assertEqual("ForeignException('java.lang.NumberFormatException: For input string: \"99\" under radix 8')", repr(e))
423492
assert True
@@ -503,10 +572,10 @@ def test_java_null_is_none(self):
503572
y = Integer.getInteger("something_that_does_not_exists2")
504573
z = None
505574

506-
assert isinstance(x, type(None))
575+
assert isinstance(x, types.NoneType)
507576
assert type(x) == polyglot.ForeignNone, type(x)
508577
assert type(None) in type(x).mro()
509-
self.assertEqual(['ForeignNone', 'NoneType', 'foreign', 'object'], [t.__name__ for t in type(x).mro()])
578+
self.assertEqual([polyglot.ForeignNone, types.NoneType, polyglot.ForeignObject, object], type(x).mro())
510579
assert repr(x) == 'None'
511580
assert str(x) == 'None'
512581

@@ -632,13 +701,23 @@ def getLevel(self):
632701
my_lr2 = MyLogRecord(Level.FINEST, message)
633702
assert my_lr2.getLevel() == Level.WARNING
634703

704+
def test_super(self):
705+
from java.util import ArrayList
706+
l = ArrayList()
707+
l.extend([5, 6, 7])
708+
l.remove(7) # Python list.remove
709+
assert l == [5, 6]
710+
711+
super(list, l).remove(0) # ArrayList#remove(int index)
712+
assert l == [6]
713+
635714
def test_java_array(self):
636715
import java
637716
il = java.type("int[]")(20)
638717

639718
assert isinstance(il, list)
640719
assert list in type(il).mro()
641-
self.assertEqual(['ForeignList', 'list', 'foreign', 'object'], [t.__name__ for t in type(il).mro()])
720+
self.assertEqual([polyglot.ForeignList, list, polyglot.ForeignObject, object], type(il).mro())
642721
assert repr(il) == repr([0] * 20)
643722
assert str(il) == str([0] * 20)
644723

@@ -695,7 +774,7 @@ def l(*elements):
695774

696775
assert isinstance(al, list)
697776
assert list in type(al).mro()
698-
self.assertEqual(['ForeignList', 'list', 'foreign', 'object'], [t.__name__ for t in type(al).mro()])
777+
self.assertEqual([polyglot.ForeignList, list, polyglot.ForeignObject, object], type(al).mro())
699778
assert repr(l(1,2)) == repr([1,2])
700779
assert str(l(1,2)) == str([1,2])
701780

@@ -850,7 +929,7 @@ def test_java_map(self):
850929

851930
assert isinstance(h, dict)
852931
assert dict in type(h).mro()
853-
self.assertEqual(['ForeignDict', 'dict', 'foreign', 'object'], [t.__name__ for t in type(h).mro()])
932+
self.assertEqual([polyglot.ForeignDict, dict, polyglot.ForeignObject, object], type(h).mro())
854933
assert repr(h) == repr({1: 2})
855934
assert str(h) == str({1: 2}), str(h)
856935
assert h
@@ -981,7 +1060,7 @@ def test_java_iterator(self):
9811060
iterator_type = type(iter([]))
9821061
assert isinstance(itr, iterator_type)
9831062
assert iterator_type in type(itr).mro()
984-
self.assertEqual(['ForeignIterator', 'iterator', 'foreign', 'object'], [t.__name__ for t in type(itr).mro()])
1063+
self.assertEqual([polyglot.ForeignIterator, iterator_type, polyglot.ForeignObject, object], type(itr).mro())
9851064
assert '<polyglot.ForeignIterator object at 0x' in repr(itr), repr(itr)
9861065
assert '<polyglot.ForeignIterator object at 0x' in str(itr), str(itr)
9871066
assert bool(itr) == True
@@ -1043,7 +1122,7 @@ def wrap(string):
10431122

10441123
assert isinstance(s, str)
10451124
assert str in type(s).mro()
1046-
self.assertEqual(['ForeignString', 'str', 'foreign', 'object'], [t.__name__ for t in type(s).mro()])
1125+
self.assertEqual([polyglot.ForeignString, str, polyglot.ForeignObject, object], type(s).mro())
10471126
assert repr(s) == repr("ab")
10481127
assert str(s) == str("ab"), str(s)
10491128
assert bool(s) == True
@@ -1053,7 +1132,7 @@ def wrap(string):
10531132
assert not empty
10541133

10551134
c = Character.valueOf(ord("A"))
1056-
self.assertEqual(['ForeignString', 'str', 'foreign', 'object'], [t.__name__ for t in type(c).mro()])
1135+
self.assertEqual([polyglot.ForeignString, str, polyglot.ForeignObject, object], type(c).mro())
10571136
assert repr(c) == repr("A")
10581137
assert str(c) == str("A"), str(s)
10591138
assert c
@@ -1152,7 +1231,7 @@ def test_foreign_number_list(self):
11521231

11531232
assert isinstance(n, list)
11541233
assert list in type(n).mro()
1155-
self.assertEqual(['ForeignNumberList', 'ForeignNumberType', 'list', 'foreign', 'object'], [t.__name__ for t in type(n).mro()])
1234+
self.assertEqual(type(n).mro(), [polyglot.ForeignNumberList, polyglot.ForeignNumber, polyglot.ForeignList, list, polyglot.ForeignObject, object])
11561235
assert repr(n) == '42', repr(n)
11571236
assert str(n) == '42', str(n)
11581237
assert n
@@ -1182,6 +1261,121 @@ def test_foreign_number_list(self):
11821261
assert l > n
11831262
assert n < l
11841263

1264+
def test_foreign_number(self):
1265+
def wrap(obj):
1266+
return __graalpython__.foreign_wrapper(obj)
1267+
1268+
def assertValueAndType(actual, expected):
1269+
self.assertEqual(expected, actual)
1270+
self.assertEqual(type(expected), type(actual))
1271+
1272+
n = wrap(42)
1273+
self.assertEqual(type(n).mro(), [polyglot.ForeignNumber, polyglot.ForeignObject, object])
1274+
assert repr(n) == '42', repr(n)
1275+
assert str(n) == '42', str(n)
1276+
assert n
1277+
1278+
assert wrap(2) + wrap(3) == 5
1279+
assert wrap(2) - wrap(3) == -1
1280+
assert wrap(2) * wrap(3) == 6
1281+
assert wrap(7) / wrap(2) == 3.5
1282+
assert wrap(7) // wrap(2) == 3
1283+
assert wrap(8) % wrap(3) == 2
1284+
assert wrap(2) ** wrap(3) == 8
1285+
assert wrap(1) << wrap(3) == 8
1286+
assert wrap(8) >> wrap(2) == 2
1287+
1288+
# 1 and not 1.0 is unfortunate but interop does not give us a way to know if a non-primitive/wrapped 3.0 is integral or floating point
1289+
assertValueAndType(wrap(3.0) // wrap(2.0), 1)
1290+
assertValueAndType(wrap(3.0) // 2.0, 1.0)
1291+
assertValueAndType(3.0 // wrap(2.0), 1.0)
1292+
1293+
assertValueAndType(wrap(3) - 2.0, 1.0)
1294+
assertValueAndType(3.0 - wrap(2), 1.0)
1295+
1296+
assert wrap(0b1110) & wrap(0b0111) == 0b0110
1297+
assert wrap(0b1110) | wrap(0b0111) == 0b1111
1298+
assert wrap(0b1110) ^ wrap(0b0111) == 0b1001
1299+
1300+
assert wrap((1 << 65) - 2) & wrap(0b111) == 0b110
1301+
assert wrap((1 << 65) - 2) | wrap(0b111) == ((1 << 65) - 1)
1302+
assert wrap((1 << 65) - 2) ^ wrap(0b1) == ((1 << 65) - 1)
1303+
1304+
assert wrap(42).as_integer_ratio() == (42, 1)
1305+
assert wrap(0b1010).bit_count() == 2
1306+
assert wrap(0b1010).bit_length() == 4
1307+
assert wrap(42).conjugate() == 42
1308+
assert wrap(42).is_integer()
1309+
assert wrap(42.0).is_integer()
1310+
assert not wrap(42.5).is_integer()
1311+
assert wrap(42.0).to_bytes() == b"*"
1312+
1313+
assert ~wrap(42) == -43
1314+
assert -wrap(42) == -42
1315+
assert +wrap(42) == 42
1316+
1317+
assertValueAndType(abs(wrap(-2)), 2)
1318+
assertValueAndType(float(wrap(2)), 2.0)
1319+
assertValueAndType(int(wrap(2.3)), 2)
1320+
assertValueAndType(math.floor(wrap(2.3)), 2)
1321+
assertValueAndType(math.ceil(wrap(2.3)), 3)
1322+
assertValueAndType(math.trunc(wrap(-2.3)), -2)
1323+
assertValueAndType(round(wrap(2.3)), 2)
1324+
1325+
missing_int_methods = set(dir(int)) - set(dir(type(wrap(1))))
1326+
missing_int_methods = [m for m in missing_int_methods if m.startswith('_') and m != '__getnewargs__']
1327+
self.assertEqual([], missing_int_methods)
1328+
1329+
missing_float_methods = set(dir(float)) - set(dir(type(wrap(1.2))))
1330+
missing_float_methods = [m for m in missing_float_methods if m.startswith('_') and m not in ('__getnewargs__', '__getformat__')]
1331+
self.assertEqual([], missing_float_methods)
1332+
1333+
def test_foreign_boolean(self):
1334+
def wrap(obj):
1335+
return __graalpython__.foreign_wrapper(obj)
1336+
1337+
def assertValueAndType(actual, expected):
1338+
self.assertEqual(expected, actual)
1339+
self.assertEqual(type(expected), type(actual))
1340+
1341+
self.assertEqual(type(wrap(True)).mro(), [polyglot.ForeignBoolean, polyglot.ForeignNumber, polyglot.ForeignObject, object])
1342+
assert repr(wrap(True)) == 'True'
1343+
assert repr(wrap(False)) == 'False'
1344+
assert str(wrap(True)) == 'True'
1345+
assert str(wrap(False)) == 'False'
1346+
assert wrap(True)
1347+
assert not wrap(False)
1348+
1349+
assert bool(wrap(True)) is True
1350+
assert bool(wrap(False)) is False
1351+
1352+
assertValueAndType(wrap(True) + wrap(2), 3)
1353+
assertValueAndType(wrap(False) + wrap(2), 2)
1354+
assertValueAndType(wrap(2) + wrap(True), 3)
1355+
assertValueAndType(wrap(2) + wrap(False), 2)
1356+
1357+
assertValueAndType(wrap(True) - wrap(2), -1)
1358+
assertValueAndType(wrap(2) - wrap(True), 1)
1359+
1360+
assert wrap(True) & wrap(True) is True
1361+
assert wrap(True) & wrap(False) is False
1362+
1363+
assert wrap(True) | wrap(False) is True
1364+
assert wrap(False) | wrap(False) is False
1365+
1366+
assert wrap(True) ^ wrap(False) is True
1367+
assert wrap(False) ^ wrap(False) is False
1368+
1369+
assertValueAndType(~wrap(True), -2)
1370+
assertValueAndType(~wrap(False), -1)
1371+
1372+
assertValueAndType(float(wrap(True)), 1.0)
1373+
assertValueAndType(int(wrap(True)), 1)
1374+
1375+
missing_bool_methods = set(dir(bool)) - set(dir(type(wrap(True))))
1376+
missing_bool_methods = [m for m in missing_bool_methods if m.startswith('_') and m != '__getnewargs__']
1377+
self.assertEqual([], missing_bool_methods)
1378+
11851379
def test_foreign_repl(self):
11861380
from java.util.logging import LogRecord
11871381
from java.util.logging import Level

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ def test_build_cache_invalidation_after_delete(self):
252252
self.assertEqual(readerClass.__name__, "Java_java.io.BufferedReader_generated")
253253

254254
__graalpython__.clear_interop_type_registry()
255-
self.assertEqual(type(reader).__name__, "foreign")
255+
self.assertEqual(type(reader).__name__, "ForeignObject")
256256

257257

258258
from java.lang import Object

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/Python3Core.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import java.util.ServiceLoader;
6060
import java.util.logging.Level;
6161

62+
import com.oracle.graal.python.builtins.objects.foreign.ForeignBooleanBuiltins;
6263
import com.oracle.graal.python.builtins.objects.foreign.ForeignNumberBuiltins;
6364
import com.oracle.graal.python.builtins.objects.type.PythonManagedClass;
6465
import com.oracle.graal.python.nodes.object.GetForeignObjectClassNode;
@@ -395,9 +396,7 @@
395396
import com.oracle.truffle.api.strings.TruffleString;
396397

397398
/**
398-
* The core is intended to the immutable part of the interpreter, including most modules and most
399-
* types. The core is embedded, using inheritance, into {@link PythonContext} to avoid indirection
400-
* through an extra field in the context.
399+
* The core is a historical artifact and PythonContext and Python3Core should be merged.
401400
*/
402401
public abstract class Python3Core {
403402
private static final int REC_LIM = 1000;
@@ -488,6 +487,7 @@ private static PythonBuiltins[] initializeBuiltins(boolean nativeAccessAllowed,
488487
new IntBuiltins(),
489488
new ForeignObjectBuiltins(),
490489
new ForeignNumberBuiltins(),
490+
new ForeignBooleanBuiltins(),
491491
new ListBuiltins(),
492492
new DictBuiltins(),
493493
new DictReprBuiltin(),

0 commit comments

Comments
 (0)