Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion py/mpconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -1124,7 +1124,7 @@ typedef time_t mp_timestamp_t;
#define MICROPY_PY_FUNCTION_ATTRS_CODE (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_FULL_FEATURES)
#endif

// Whether to support the descriptors __get__, __set__, __delete__
// Whether to support the descriptors __get__, __set__, __delete__, __set_name__
// This costs some code size and makes load/store/delete of instance
// attributes slower for the classes that use this feature
#ifndef MICROPY_PY_DESCRIPTORS
Expand Down
52 changes: 49 additions & 3 deletions py/objtype.c
Original file line number Diff line number Diff line change
Expand Up @@ -661,8 +661,8 @@ static void mp_obj_instance_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *des
// try __getattr__
if (attr != MP_QSTR___getattr__) {
#if MICROPY_PY_DESCRIPTORS
// With descriptors enabled, don't delegate lookups of __get__/__set__/__delete__.
if (attr == MP_QSTR___get__ || attr == MP_QSTR___set__ || attr == MP_QSTR___delete__) {
// With descriptors enabled, don't delegate lookups of __get__/__set__/__delete__/__set_name__.
if (attr == MP_QSTR___get__ || attr == MP_QSTR___set__ || attr == MP_QSTR___delete__ || attr == MP_QSTR___set_name__) {
return;
}
#endif
Expand Down Expand Up @@ -960,7 +960,7 @@ static bool check_for_special_accessors(mp_obj_t key, mp_obj_t value) {
#endif
#if MICROPY_PY_DESCRIPTORS
static const uint8_t to_check[] = {
MP_QSTR___get__, MP_QSTR___set__, MP_QSTR___delete__,
MP_QSTR___get__, MP_QSTR___set__, MP_QSTR___delete__, // not needed for MP_QSTR___set_name__ tho
};
for (size_t i = 0; i < MP_ARRAY_SIZE(to_check); ++i) {
mp_obj_t dest_temp[2];
Expand All @@ -974,6 +974,48 @@ static bool check_for_special_accessors(mp_obj_t key, mp_obj_t value) {
}
#endif

#if MICROPY_PY_DESCRIPTORS
static void run_set_name_hooks(mp_map_t *locals_map_orig, mp_obj_t owner) {
// copy the dict so we can iterate safely even while __set_name__ potentially modifies the original
mp_map_t locals_map = *locals_map_orig;
locals_map.table = mp_local_alloc(locals_map.alloc * sizeof(mp_map_elem_t));
memcpy(locals_map.table, locals_map_orig->table, locals_map.alloc * sizeof(mp_map_elem_t));

#if MICROPY_ENABLE_PYSTACK
nlr_buf_t nlr;
if (nlr_push(&nlr) == 0)
// Note: on !MICROPY_ENABLE_PYSTACK ports, `mp_local_alloc` is just `alloca` and `mp_local_free` is a no-op.
// Therefore we don't need to set an exception trap; the exception handler implicitly frees as it unwinds the stack.
#endif
{
// use the copy to call __set_name__ on each
for (size_t i = 0; i < locals_map.alloc; i++) {
if (mp_map_slot_is_filled(&locals_map, i)) {
mp_map_elem_t *elem = &(locals_map.table[i]);
mp_obj_t set_name_method[4];
mp_load_method_maybe(elem->value, MP_QSTR___set_name__, set_name_method);
if (set_name_method[1] != MP_OBJ_NULL) {
set_name_method[2] = owner;
set_name_method[3] = elem->key;
mp_call_method_n_kw(2, 0, set_name_method);
}
}
}

#if MICROPY_ENABLE_PYSTACK
nlr_pop();
#endif
mp_local_free(locals_map.table);
}
#if MICROPY_ENABLE_PYSTACK
else {
mp_local_free(locals_map.table);
nlr_raise(nlr.ret_val); // TODO cpython raises a RuntimeError in this situation
}
#endif
}
#endif

static void type_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
(void)kind;
mp_obj_type_t *self = MP_OBJ_TO_PTR(self_in);
Expand Down Expand Up @@ -1241,6 +1283,10 @@ mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict)
}
}

#if MICROPY_PY_DESCRIPTORS
run_set_name_hooks(locals_map, MP_OBJ_FROM_PTR(o));
#endif

return MP_OBJ_FROM_PTR(o);
}

Expand Down
20 changes: 13 additions & 7 deletions tests/basics/class_descriptor.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
class Descriptor:
def __get__(self, obj, cls):
print('get')
print("get")
print(type(obj) is Main)
print(cls is Main)
return 'result'
return "result"

def __set__(self, obj, val):
print('set')
print("set")
print(type(obj) is Main)
print(val)

def __delete__(self, obj):
print('delete')
print("delete")
print(type(obj) is Main)

def __set_name__(self, owner, name):
print("set_name", name)
print(owner.__name__ == "Main")


class Main:
Forward = Descriptor()


m = Main()
try:
m.__class__
Expand All @@ -26,15 +32,15 @@ class Main:
raise SystemExit

r = m.Forward
if 'Descriptor' in repr(r.__class__):
if "Descriptor" in repr(r.__class__):
# Target doesn't support descriptors.
print('SKIP')
print("SKIP")
raise SystemExit

# Test assignment and deletion.

print(r)
m.Forward = 'a'
m.Forward = "a"
del m.Forward

# Test that lookup of descriptors like __get__ are not passed into __getattr__.
Expand Down
179 changes: 179 additions & 0 deletions tests/basics/class_setname_hazard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
def skip_if_no_descriptors():
class Descriptor:
def __get__(self, obj, cls):
return

class TestClass:
Forward = Descriptor()

a = TestClass()
try:
a.__class__
except AttributeError:
# Target doesn't support __class__.
print("SKIP")
raise SystemExit

b = a.Forward
if "Descriptor" in repr(b.__class__):
# Target doesn't support descriptors.
print("SKIP")
raise SystemExit


skip_if_no_descriptors()


# Test basic hazard-free mutations of the enclosing class.


class GetSibling:
def __set_name__(self, owner, name):
print(getattr(owner, name + "_sib"))


class GetSiblingTest:
desc = GetSibling()
desc_sib = 111


t110 = GetSiblingTest()


class SetSibling:
def __set_name__(self, owner, name):
setattr(owner, name + "_sib", 121)


class SetSiblingTest:
desc = SetSibling()


t120 = SetSiblingTest()

print(t120.desc_sib)


class DelSibling:
def __set_name__(self, owner, name):
delattr(owner, name + "_sib")


class DelSiblingTest:
desc = DelSibling()
desc_sib = 131


t130 = DelSiblingTest()

try:
print(t130.desc_sib)
except AttributeError:
print("AttributeError")


class GetSelf:
x = 211

def __set_name__(self, owner, name):
print(getattr(owner, name).x)


class GetSelfTest:
desc = GetSelf()


t210 = GetSelfTest()


class SetSelf:
def __set_name__(self, owner, name):
setattr(owner, name, 221)


class SetSelfTest:
desc = SetSelf()


t220 = SetSelfTest()

print(t220.desc)


class DelSelf:
def __set_name__(self, owner, name):
delattr(owner, name)


class DelSelfTest:
desc = DelSelf()


t230 = DelSelfTest()

try:
print(t230.desc)
except AttributeError:
print("AttributeError")


# Test exception behavior


class Raise:
def __set_name__(self, owner, name):
raise Exception()


try:

class RaiseTest:
desc = Raise()
except Exception as e:
print("Exception")


# Test simple hazards: whether other class attributes still get __set_name__ even if removed before being run


class SetSpecific:
def __init__(self, sib_name, sib_replace):
self.sib_name = sib_name
self.sib_replace = sib_replace

def __set_name__(self, owner, name):
setattr(owner, self.sib_name, self.sib_replace)


class SetReplaceTest:
a = SetSpecific("b", 312) # one of these is changed first
b = SetSpecific("a", 311)


t310 = SetReplaceTest()
print(t310.a)
print(t310.b)


class DelSpecific:
def __init__(self, sib_name):
self.sib_name = sib_name

def __set_name__(self, owner, name):
delattr(owner, self.sib_name)


class DelReplaceTest:
a = DelSpecific("b") # one of these is removed first
b = DelSpecific("a")


t320 = DelReplaceTest()
try:
print(t320.a)
except AttributeError:
print("AttributeError")
try:
print(t320.b)
except AttributeError:
print("AttributeError")
Loading
Loading