Skip to content

py/objtype: Add support for __set_name__. (hazard version) #15503

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
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
23 changes: 20 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 Down Expand Up @@ -1241,6 +1241,23 @@ mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict)
}
}

#if MICROPY_PY_DESCRIPTORS
// call __set_name__ on all entries (especially descriptors)
for (size_t i = 0; i < locals_map->alloc; i++) {
if (mp_map_slot_is_filled(locals_map, i)) {
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] = MP_OBJ_FROM_PTR(o);
set_name_method[3] = elem->key;
mp_call_method_n_kw(2, 0, set_name_method);
}
}
}
#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
118 changes: 118 additions & 0 deletions tests/basics/class_setname_hazard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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.
# See also cpydiff/core_class_setname_hazard.py for broken non-hazard-free cases.


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


class GetSiblingTest:
desc = GetSibling()
desc_sib = 101


t101 = GetSiblingTest()


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


class SetSiblingTest:
desc = SetSibling()


t102 = SetSiblingTest()

print(t102.desc_sib)


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


class DelSiblingTest:
desc = DelSibling()
desc_sib = 103


t103 = DelSiblingTest()

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


class GetSelf:
x = 201

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


class GetSelfTest:
desc = GetSelf()


t201 = GetSelfTest()


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


class SetSelfTest:
desc = SetSelf()


t202 = SetSelfTest()

print(t202.desc)


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


class DelSelfTest:
desc = DelSelf()


t203 = DelSelfTest()

try:
print(t203.desc)
except AttributeError:
print("AttributeError")
87 changes: 87 additions & 0 deletions tests/cpydiff/core_class_setname_hazard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""
categories: Core,Classes
description: Descriptor ``__set_name__`` functions may be called twice or missed if members of the parent class are created or removed in ``__set_name__``.
cause: The ``__set_name__`` procedure is not isolated from the underlying modify-while-iter sequence hazard of the underlying class ``__dict__``.
workaround: Avoid ``__set_name__`` implementations that add or remove members from the parent class.
"""

# This bug is EXTREMELY difficult to demonstrate with only a minimal test case
# due to the unstable iteration order of a class's namespace. This bug more-or-less
# _requires_ a stochastic test in order to guarantee it occurs or demonstrate its
# potential impact with any level of clarity.

import random
import re

letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

junk_re = re.compile(r"[A-Z][A-Z][A-Z][A-Z][A-Z]")


def junk_fill(obj, n=10): # Add randomly-generated attributes to an object.
for i in range(n):
name = "".join(random.choice(letters) for j in range(5))
setattr(obj, name, object())


def junk_clear(obj): # Remove attributes added by junk_fill.
to_del = [name for name in dir(obj) if junk_re.match(name)]
for name in to_del:
delattr(obj, name)


def junk_sequencer():
global runs
try:
while True:
owner, name = yield
runs[name] = runs.get(name, 0) + 1
junk_fill(owner)
finally:
junk_clear(owner)


class JunkMaker:
def __set_name__(self, owner, name):
global seq
seq.send((owner, name))


runs = {}
seq = junk_sequencer()
next(seq)


class Main:
a = JunkMaker()
b = JunkMaker()
c = JunkMaker()
d = JunkMaker()
e = JunkMaker()
f = JunkMaker()
g = JunkMaker()
h = JunkMaker()
i = JunkMaker()
j = JunkMaker()
k = JunkMaker()
l = JunkMaker()
m = JunkMaker()
n = JunkMaker()
o = JunkMaker()
p = JunkMaker()
q = JunkMaker()
r = JunkMaker()
s = JunkMaker()
t = JunkMaker()
u = JunkMaker()
v = JunkMaker()
w = JunkMaker()
x = JunkMaker()
y = JunkMaker()
z = JunkMaker()


seq.close()

for k in letters.lower():
print(k, runs.get(k, 0))
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy