Skip to content

py/objtype: Add basic __init_subclass__ metaclass support. #15511

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 12 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
1 change: 1 addition & 0 deletions ports/windows/mpconfigport.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
#define MICROPY_VFS_POSIX (1)
#define MICROPY_PY_FUNCTION_ATTRS (1)
#define MICROPY_PY_DESCRIPTORS (1)
#define MICROPY_PY_METACLASSES (1)
#define MICROPY_PY_DELATTR_SETATTR (1)
#define MICROPY_PY_FSTRINGS (1)
#define MICROPY_PY_BUILTINS_BYTES_HEX (1)
Expand Down
8 changes: 7 additions & 1 deletion py/mpconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -1031,13 +1031,19 @@ typedef double mp_float_t;
#define MICROPY_PY_FUNCTION_ATTRS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_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
#define MICROPY_PY_DESCRIPTORS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
#endif

// Whether to support metaclass functionality (currently just __init_subclass__)
// This costs some code size and makes class creation slower
#ifndef MICROPY_PY_METACLASSES
#define MICROPY_PY_METACLASSES (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
#endif

// Whether to support class __delattr__ and __setattr__ methods
// This costs some code size and makes store/delete of instance
// attributes slower for the classes that use this feature
Expand Down
18 changes: 18 additions & 0 deletions py/objobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,21 @@ static mp_obj_t object___delattr__(mp_obj_t self_in, mp_obj_t attr) {
static MP_DEFINE_CONST_FUN_OBJ_2(object___delattr___obj, object___delattr__);
#endif

#if MICROPY_PY_METACLASSES
static mp_obj_t object___init_subclass__(mp_obj_t cls_in) {
// call over to the next base, essentially `super(object, cls).__init_subclass__()`
// (but if this is the last base, we're done)
// mp_obj_t init_subclass_method[2] = {&mp_type_object, cls_in};
// mp_load_super_method_maybe(MP_QSTR___init_subclass__, init_subclass_method);
// if (init_subclass_method[1] != MP_OBJ_NULL) {
// mp_call_method_n_kw(0, 0, init_subclass_method);
// }
return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_1(object___init_subclass___fun_obj, object___init_subclass__);
static MP_DEFINE_CONST_CLASSMETHOD_OBJ(object___init_subclass___obj, MP_ROM_PTR(&object___init_subclass___fun_obj));
#endif

static const mp_rom_map_elem_t object_locals_dict_table[] = {
#if MICROPY_CPYTHON_COMPAT
{ MP_ROM_QSTR(MP_QSTR___init__), MP_ROM_PTR(&object___init___obj) },
Expand All @@ -106,6 +121,9 @@ static const mp_rom_map_elem_t object_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR___setattr__), MP_ROM_PTR(&object___setattr___obj) },
{ MP_ROM_QSTR(MP_QSTR___delattr__), MP_ROM_PTR(&object___delattr___obj) },
#endif
#if MICROPY_PY_METACLASSES
{ MP_ROM_QSTR(MP_QSTR___init_subclass__), MP_ROM_PTR(&object___init_subclass___obj) },
#endif
};

static MP_DEFINE_CONST_DICT(object_locals_dict, object_locals_dict_table);
Expand Down
44 changes: 44 additions & 0 deletions py/objtype.c
Original file line number Diff line number Diff line change
Expand Up @@ -1234,6 +1234,50 @@ mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict)
}
}

#if MICROPY_PY_METACLASSES
// __init_subclass__ is a special-cased classmethod in CPython
// See: https://github.com/python/cpython/blob/3de092b82f5aa02fa293cd654c2ab26556ecf703/Objects/typeobject.c#L4247
elem = mp_map_lookup(locals_map, MP_OBJ_NEW_QSTR(MP_QSTR___init_subclass__), MP_MAP_LOOKUP);
if (elem != NULL) {
// __init_subclass__ slot exists; check if it is a function
if (mp_obj_is_fun(elem->value)) {
// __init_subclass__ is a function, wrap it in a classmethod decorator
elem->value = static_class_method_make_new(&mp_type_classmethod, 1, 0, &elem->value);
}
}
#endif

// CPython calls __set_name__ on class members before calling __init_subclass__ on bases.
// See: https://github.com/python/cpython/blob/3de092b82f5aa02fa293cd654c2ab26556ecf703/Objects/typeobject.c#L4369-L4375
#if MICROPY_PY_DESCRIPTORS | MICROPY_PY_METACLASSES
// 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

#if MICROPY_PY_METACLASSES
// call __init_subclass__ from each base class
for (size_t i = 0; i < bases_len; i++) {
mp_obj_t init_subclass_method[2];
mp_load_method_maybe(bases_items[i], MP_QSTR___init_subclass__, init_subclass_method);
if (init_subclass_method[1] != MP_OBJ_NULL) {
init_subclass_method[1] = MP_OBJ_FROM_PTR(o);
mp_call_method_n_kw(0, 0, init_subclass_method);
}
}
#endif

return MP_OBJ_FROM_PTR(o);
}

Expand Down
35 changes: 35 additions & 0 deletions tests/basics/class_initsubclass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# skip failing on minimal port
try:

class T:
pass

T().__class__
getattr

def print_check_all_attrs(obj, name: str, attrs: "list[str]"):
for attr in attrs:
print(name, attr, getattr(obj, attr, "missing"))

except AttributeError:
print("SKIP")
raise SystemExit


class A:
a = "A"

def __init_subclass__(cls):
print("A init_subclass", cls.__name__)
cls.aa = "AA"

print("class A")


class B(A):
b = "B"
print("class B")


print_check_all_attrs(A, "A", ["a", "aa", "b"])
print_check_all_attrs(B, "B", ["a", "aa", "b"])
52 changes: 52 additions & 0 deletions tests/basics/class_initsubclass_recursive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# skip failing on minimal port
try:

class T:
pass

T().__class__
getattr

def print_check_all_attrs(obj, name: str, attrs: "list[str]"):
for attr in attrs:
print(name, attr, getattr(obj, attr, "missing"))

except AttributeError:
print("SKIP")
raise SystemExit


class A:
a = "A"

def __init_subclass__(cls):
print("A init_subclass", cls.__name__)
cls.aa = "AA"

print("class A")


class B(A):
b = "B"

def __init_subclass__(cls):
super(B, cls).__init_subclass__()
print("B init_subclass", cls.__name__)
cls.bb = "BB"

print("class B")


class C(B):
c = "C"
print("class C")


def print_check_all_attrs(obj, name: str, attrs: "list[str]"):
for attr in attrs:
print(name, attr, getattr(obj, attr, "missing"))


print_check_all_attrs(A, "A", ["a", "aa", "b", "bb", "c"])
print_check_all_attrs(B, "B", ["a", "aa", "b", "bb", "c"])
print_check_all_attrs(C, "C", ["a", "aa", "b", "bb", "c"])
18 changes: 18 additions & 0 deletions tests/basics/class_setname.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# test calling __set_name__ during class creation
# https://docs.python.org/3/reference/datamodel.html#object.__set_name__

class A:
def __set_name__(self, owner, name):
print("owner", owner.__name__)
print("name", name)

class B:
a = A()

# skip failing on minimal port without descriptor support
b = B()
try:
b.__class__
except AttributeError:
print("SKIP")
raise SystemExit
19 changes: 19 additions & 0 deletions tests/cpydiff/core_class_initsubclass_kw.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
categories: Core,Classes
description: Keyword arguments are not passed to __init_subclass__.
cause: Micropython doesn't allow kwargs in a base class list.
workaround: Unknown
"""


class Philosopher:
def __init_subclass__(cls, default_name, **kwargs):
super().__init_subclass__(**kwargs)
cls.default_name = default_name


class AustralianPhilosopher(Philosopher, default_name="Bruce"):
pass


print(AustralianPhilosopher.default_name)
77 changes: 77 additions & 0 deletions tests/cpydiff/core_class_initsubclass_multi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""
categories: Core,Classes
description: Micropython calls __init_subclass__ directly for all direct base classes.
cause: Micropython can't rely on __init_subclass__ implementations calling super().__init_subclass__() to recurse through ancestor classes like CPython.
workaround: Omit calling super().__init_subclass__() in __init_subclass__ implementations.
"""


# In CPython, only the first base__init_subclass__
class A1:
a = "A1"

def __init_subclass__(cls):
print("A1 init_subclass", cls.__name__)
cls.aa = "AA"

print("class A1")


class B1:
b = "B1"

def __init_subclass__(cls):
print("B1 init_subclass", cls.__name__)
cls.bb = "BB"

print("class B1")


class C1(A1, B1):
c = "C1"
print("class C1")


# In CPython it's specified to call super().__init_subclass__() in __init_subclass__.
# But the presence of super() makes the invocation of __init_subclass__ fail.
class A2:
a = "A2"

@classmethod
def __init_subclass__(cls):
super().__init_subclass__()
print("A2 init_subclass", cls.__name__)
cls.aa = "AA"

print("class A2")


class B2:
b = "B2"

@classmethod
def __init_subclass__(cls):
super().__init_subclass__()
print("B2 init_subclass", cls.__name__)
cls.bb = "BB"

print("class B2")


class C2(A2, B2):
c = "C2"
print("class C2")


def print_check_all_attrs(obj, name: str, attrs: "list[str]"):
for attr in attrs:
print(name, attr, getattr(obj, attr, "missing"))


print_check_all_attrs(A1, "A1", ["a", "aa", "b", "bb", "c"])
print_check_all_attrs(B1, "B1", ["a", "aa", "b", "bb", "c"])
print_check_all_attrs(C1, "C1", ["a", "aa", "b", "bb", "c"])

print_check_all_attrs(A2, "A2", ["a", "aa", "b", "bb", "c"])
print_check_all_attrs(B2, "B2", ["a", "aa", "b", "bb", "c"])
print_check_all_attrs(C2, "C2", ["a", "aa", "b", "bb", "c"])
42 changes: 42 additions & 0 deletions tests/cpydiff/core_class_initsubclass_recursive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
categories: Core,Classes
description: Micropython can't recurse through base classes for __init_subclass__.
cause: Micropython doesn't support super() inside classmethods.
workaround: Unknown
"""


class A:
a = "A"

def __init_subclass__(cls):
print("A init_subclass", cls.__name__)
cls.aa = "AA"

print("class A")


class B(A):
b = "B"

def __init_subclass__(cls):
super().__init_subclass__()
print("B init_subclass", cls.__name__)
cls.bb = "BB"

print("class B")


class C(B):
c = "C"
print("class C")


def print_check_all_attrs(obj, name: str, attrs: "list[str]"):
for attr in attrs:
print(name, attr, getattr(obj, attr, "missing"))


print_check_all_attrs(A, "A", ["a", "aa", "b", "bb", "c"])
print_check_all_attrs(B, "B", ["a", "aa", "b", "bb", "c"])
print_check_all_attrs(C, "C", ["a", "aa", "b", "bb", "c"])
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