Skip to content

py/objtype: Add support for PEP487 __set_name__. #16806

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
7 changes: 6 additions & 1 deletion docs/differences/python_36.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ Python 3.6 beta 1 was released on 12 Sep 2016, and a summary of the new features
+--------------------------------------------------------+--------------------------------------------------+-----------------+
| `PEP 468 <https://www.python.org/dev/peps/pep-0468/>`_ | Preserving the order of *kwargs* in a function | |
+--------------------------------------------------------+--------------------------------------------------+-----------------+
| `PEP 487 <https://www.python.org/dev/peps/pep-0487/>`_ | Simpler customization of class creation | |
| `PEP 487 <https://www.python.org/dev/peps/pep-0487/>`_ | Simpler customization of class creation | Partial |
| | | [#setname]_ |
+--------------------------------------------------------+--------------------------------------------------+-----------------+
| `PEP 520 <https://www.python.org/dev/peps/pep-0520/>`_ | Preserving Class Attribute Definition Order | |
+--------------------------------------------------------+--------------------------------------------------+-----------------+
Expand Down Expand Up @@ -198,3 +199,7 @@ Changes to built-in modules:
+--------------------------------------------------------------------------------------------------------------+----------------+
| The *compress()* and *decompress()* functions now accept keyword arguments | |
+--------------------------------------------------------------------------------------------------------------+----------------+

.. rubric:: Notes

.. [#setname] Only :func:`__set_name__` is implemented.
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
93 changes: 80 additions & 13 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,51 @@ static bool check_for_special_accessors(mp_obj_t key, mp_obj_t value) {
}
#endif

#if MICROPY_PY_DESCRIPTORS
// Shared data layout for the __set_name__ call and a linked list of calls to be made.
typedef union _setname_list_t setname_list_t;
union _setname_list_t {
mp_obj_t call[4];
struct {
mp_obj_t _meth;
mp_obj_t _self;
setname_list_t *next; // can use the "owner" argument position temporarily for the linked list
mp_obj_t _name;
};
};

// Append any `__set_name__` method on `value` to the setname list, with its per-attr args
static setname_list_t *setname_maybe_bind_append(setname_list_t *tail, mp_obj_t name, mp_obj_t value) {
// make certain our type-punning is safe:
MP_STATIC_ASSERT_NONCONSTEXPR(offsetof(setname_list_t, next) == offsetof(setname_list_t, call[2]));

// tail is a blank list entry
mp_load_method_maybe(value, MP_QSTR___set_name__, tail->call);
if (tail->call[1] != MP_OBJ_NULL) {
// Each time a __set_name__ is found, leave it in-place in the former tail and allocate a new tail
tail->next = m_new_obj(setname_list_t);
tail->next->next = NULL;
tail->call[3] = name;
return tail->next;
} else {
return tail;
}
}

// Execute the captured `__set_name__` calls, destroying the setname list in the process.
static void setname_consume_call_all(setname_list_t head, mp_obj_t owner) {
setname_list_t *iter = &head, *next;
while ((next = iter->next) != NULL) {
iter->call[2] = owner;
mp_call_method_n_kw(2, 0, iter->call);
if (iter != &head) { // head is stack, just mark blank
m_del_obj(setname_list_t, iter); // optimistic free, help gc not take too many passes to unwind
}
iter = next;
}
}
#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 @@ -1210,20 +1255,38 @@ mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict)
}
}

#if MICROPY_PY_DESCRIPTORS
// To avoid any dynamic allocations when no __set_name__ exists,
// the head of this list is kept on the stack (marked blank with `next = NULL`).
setname_list_t setname_list = { .next = NULL };
setname_list_t *setname_tail = &setname_list;
#endif

#if ENABLE_SPECIAL_ACCESSORS
// Check if the class has any special accessor methods
if (!(o->flags & MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS)) {
for (size_t i = 0; i < locals_ptr->map.alloc; i++) {
if (mp_map_slot_is_filled(&locals_ptr->map, i)) {
const mp_map_elem_t *elem = &locals_ptr->map.table[i];
if (check_for_special_accessors(elem->key, elem->value)) {
o->flags |= MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS;
break;
}
// Check if the class has any special accessor methods,
// and accumulate bound __set_name__ methods that need to be called
for (size_t i = 0; i < locals_ptr->map.alloc; i++) {
#if !MICROPY_PY_DESCRIPTORS
// __set_name__ needs to scan the entire locals map, can't early-terminate
if (o->flags & MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS) {
break;
}
#endif

if (mp_map_slot_is_filled(&locals_ptr->map, i)) {
const mp_map_elem_t *elem = &locals_ptr->map.table[i];

if (!(o->flags & MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS) // elidable when the early-termination check is enabled
&& check_for_special_accessors(elem->key, elem->value)) {
o->flags |= MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS;
}

#if MICROPY_PY_DESCRIPTORS
setname_tail = setname_maybe_bind_append(setname_tail, elem->key, elem->value);
#endif
}
}
#endif
#endif // ENABLE_SPECIAL_ACCESSORS

const mp_obj_type_t *native_base;
size_t num_native_bases = instance_count_native_bases(o, &native_base);
Expand All @@ -1241,6 +1304,10 @@ mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict)
}
}

#if MICROPY_PY_DESCRIPTORS
setname_consume_call_all(setname_list, 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
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