Skip to content

Commit 928c032

Browse files
committed
rework KEYS and KEY_INFO to determine side effects and return all inherited attributes
1 parent a0a1b0d commit 928c032

File tree

3 files changed

+197
-75
lines changed

3 files changed

+197
-75
lines changed

doc/INTEROP.md

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,12 @@ Returns true for None only.
145145

146146
###### HAS_SIZE
147147
According to the Truffle interop contract answering `true` to `HAS_SIZE` implies
148-
that indexed element access is available. Thus, we answer `true` here only for
149-
(sub-)instances of `tuple`, `list`, `array.array`, `bytearray`, `bytes`, `str`,
150-
and `range`.
148+
that indexed element access is available. However, we cannot fully guarantee
149+
this. We may answer `true` here when the object has both a `__len__` field and a
150+
`__getitem__` field. If the object's length is reported >0, we also try to read
151+
the item `0` and if that fails, we answer `false`. If the object reports it's
152+
empty, we cannot know if a read with an index will actually work, but we'll
153+
report `true`.
151154

152155
###### GET_SIZE
153156
Calls `__len__`. Just because `GET_SIZE` returns something positive does not
@@ -159,27 +162,35 @@ knowing what the `__getitem__` method does with an integer argument. Use
159162
Returns true for those values that can be unboxed using the `UNBOX` message.
160163

161164
###### KEY_INFO
162-
This will lookup the key using `READ`, assume it is readable and writable, and
163-
check `IS_EXECUTABLE`.
165+
This will lookup the key using the Python MRO. It will check if it's readable
166+
and writable, and also check if it has side-effects based on wether it is an
167+
inherited descriptor (i.e., an object with `__get__`, `__set__`, and/or
168+
`__delete__`). If the owner of the key is mutable (the owner being the class the
169+
key is inherited from or the object itself) then `REMOVABLE` and `MODIFABLE` are
170+
true. If the object itself is mutable, `INSERTABLE` will also be true. Finally,
171+
if the attribute is a function or it is *not* a descriptor and has a `__call__`,
172+
we declare it `INOCABLE`. We don't do this for descriptors, because we would
173+
have to run the `__get__` method and this message should not have side-effects.
164174

165175
###### HAS_KEYS
166-
Returns true for any boxed Python object, so small integers, booleans, or floats
167-
usually don't return true.
176+
Always returns true.
168177

169178
###### KEYS
170-
This returns the direct attributes of the receiver object, which would usually
171-
be available through `__getattribute__`.
172-
173-
The `KEYS` message requires the returned object to have only `java.lang.String`
174-
items. If the object responds to `keys`, `values`, `items`, and `__getitem__`,
175-
we assume it is Mapping, and we present the result of the `keys` method in
176-
combination with the attributes if, and only if, all keys are strings. This is
177-
roughly parallel to how `READ` and `WRITE` would be handled for string keys.
179+
This returns the all attributes of the receiver object that would usually be
180+
available through `__getattribute__`, i.e., both inherited and direct
181+
attributes.
182+
183+
If the object responds to `keys`, `values`, `items`, and `__getitem__`, we
184+
assume it is Mapping, and we present the String result of the `keys` method in
185+
combination with the attributes, prefixed with `[` if, and only if, the request
186+
asked for _internal_ keys also. The `KEYS` message requires the returned object
187+
to have only `java.lang.String` items, so inlo String keys are added to the
188+
result set. The `[` prefix ensures that in our handling of `READ` and `WRITE`
189+
messages we also treat them as mapping entries, not attributes.
178190

179191
It's still possible that none of the keys can be `READ`: the `READ` message uses
180192
Python semantics for lookup, which means that an inherited descriptor with a
181-
`__get__` method may still come before the object's keys and do anything
182-
(including raising an `AttributeError`).
193+
`__get__` method or the `__getitem__` method may still intercept actual access.
183194

184195
###### IS_POINTER
185196
Returns true if the object is a Python function defined in a Python C extension

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

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ def test_import():
4747
import python_cext
4848
assert imported_cext is python_cext
4949

50+
class GetterOnly():
51+
def __get__(self, instance, owner):
52+
pass
53+
5054
class CustomObject():
5155
field = 42
5256

@@ -56,6 +60,16 @@ def __getitem__(self, item):
5660
def __len__(self):
5761
return 21
5862

63+
getter = GetterOnly()
64+
65+
@property
66+
def setter(self):
67+
pass
68+
69+
@setter.setter
70+
def setter_setter(self):
71+
pass
72+
5973
class CustomMutable(CustomObject):
6074
_items = {}
6175

@@ -195,11 +209,31 @@ def test_has_keys():
195209

196210
def test_keys():
197211
o = CustomObject()
198-
assert len(polyglot.__keys__(o)) == 0
212+
inherited_keys = len(polyglot.__keys__(o))
199213
o.my_field = 1
200-
assert len(polyglot.__keys__(o)) == 1
214+
assert len(polyglot.__keys__(o)) == 1 + inherited_keys
201215
assert "my_field" in polyglot.__keys__(o)
202216

217+
def test_key_info():
218+
o = CustomObject()
219+
o.my_field = 1
220+
o.test_exec = lambda: False
221+
readable_invokable_insertable = polyglot.__key_info__(o, "__init__")
222+
223+
readable_invokable_modifiable_insertable = polyglot.__key_info__(o, "__len__")
224+
assert readable_invokable_modifiable_insertable == polyglot.__key_info__(o, "test_exec")
225+
226+
readable_modifiable_insertable = polyglot.__key_info__(o, "field")
227+
assert readable_modifiable_insertable == polyglot.__key_info__(o, "my_field")
228+
229+
assert readable_invokable_insertable != readable_modifiable_insertable
230+
assert readable_invokable_insertable != readable_invokable_modifiable_insertable
231+
assert readable_modifiable_insertable != readable_invokable_modifiable_insertable
232+
233+
sideeffects_get = polyglot.__key_info__(o, "getter")
234+
sideeffects_get_set = polyglot.__key_info__(o, "setter")
235+
assert sideeffects_get != sideeffects_get_set
236+
203237
def test_host_lookup():
204238
import java
205239
try:

0 commit comments

Comments
 (0)