Skip to content

Commit 5c3e108

Browse files
committed
py: Add metaclass __prepare__ method support (PEP 3115).
Implements the __prepare__ metaclass method which allows metaclasses to customize the namespace dictionary before class body execution. This is required for enum.auto() support and other advanced metaclass patterns. Changes: - py/mpconfig.h: Add MICROPY_PY_METACLASS_PREPARE configuration flag - py/modbuiltins.c: Reorder __build_class__ to call __prepare__ before class body execution - tests/basics/class_metaclass_prepare.py: Add comprehensive tests Size impact: ~152 bytes when enabled (measured on Unix port) Follows the implementation approach from PR micropython#18362. Signed-off-by: Andrew Leech <[email protected]>
1 parent ccecacb commit 5c3e108

File tree

4 files changed

+189
-21
lines changed

4 files changed

+189
-21
lines changed

py/modbuiltins.c

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,39 +49,61 @@ extern struct _mp_dummy_t mp_sys_stdout_obj; // type is irrelevant, just need po
4949
static mp_obj_t mp_builtin___build_class__(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs) {
5050
assert(2 <= n_args);
5151

52-
// set the new classes __locals__ object
53-
mp_obj_dict_t *old_locals = mp_locals_get();
54-
mp_obj_t class_locals = mp_obj_new_dict(0);
55-
mp_locals_set(MP_OBJ_TO_PTR(class_locals));
56-
57-
// call the class code
58-
mp_obj_t cell = mp_call_function_0(args[0]);
59-
60-
// restore old __locals__ object
61-
mp_locals_set(old_locals);
62-
63-
// get the class type (meta object) from the base objects
52+
// STEP 1: Determine metaclass FIRST (before creating namespace)
6453
mp_obj_t meta;
6554
mp_map_elem_t *key_elem = mp_map_lookup(kwargs, MP_OBJ_NEW_QSTR(MP_QSTR_metaclass), MP_MAP_LOOKUP);
6655
if (key_elem != NULL) {
6756
meta = key_elem->value;
6857
} else {
69-
if (n_args == 2) {
70-
// no explicit bases, so use 'type'
71-
meta = MP_OBJ_FROM_PTR(&mp_type_type);
72-
} else {
73-
// use type of first base object
74-
meta = MP_OBJ_FROM_PTR(mp_obj_get_type(args[2]));
75-
}
58+
if (n_args == 2) {
59+
// no explicit bases, so use 'type'
60+
meta = MP_OBJ_FROM_PTR(&mp_type_type);
61+
} else {
62+
// use type of first base object
63+
meta = MP_OBJ_FROM_PTR(mp_obj_get_type(args[2]));
64+
}
7665
}
7766

7867
// TODO do proper metaclass resolution for multiple base objects
7968

80-
// create the new class using a call to the meta object
69+
// STEP 2: Call __prepare__ if it exists, or create regular dict
70+
mp_obj_t class_locals;
71+
#if MICROPY_PY_METACLASS_PREPARE
72+
mp_obj_t prepare_dest[2] = {MP_OBJ_NULL, MP_OBJ_NULL};
73+
mp_load_method_maybe(meta, MP_QSTR___prepare__, prepare_dest);
74+
75+
if (prepare_dest[0] != MP_OBJ_NULL) {
76+
// __prepare__ exists, call it with (name, bases)
77+
mp_obj_t prepare_args[4];
78+
prepare_args[0] = prepare_dest[0]; // method function
79+
prepare_args[1] = prepare_dest[1]; // self (metaclass)
80+
prepare_args[2] = args[1]; // class name
81+
prepare_args[3] = mp_obj_new_tuple(n_args - 2, args + 2); // bases tuple
82+
class_locals = mp_call_method_n_kw(2, 0, prepare_args);
83+
} else {
84+
// No __prepare__, use regular dict
85+
class_locals = mp_obj_new_dict(0);
86+
}
87+
#else
88+
// __prepare__ not supported, always use regular dict
89+
class_locals = mp_obj_new_dict(0);
90+
#endif
91+
92+
// STEP 3: Execute class body with the namespace from __prepare__
93+
mp_obj_dict_t *old_locals = mp_locals_get();
94+
mp_locals_set(MP_OBJ_TO_PTR(class_locals));
95+
96+
// call the class code
97+
mp_obj_t cell = mp_call_function_0(args[0]);
98+
99+
// restore old __locals__ object
100+
mp_locals_set(old_locals);
101+
102+
// STEP 4: Create the class using the metaclass
81103
mp_obj_t meta_args[3];
82104
meta_args[0] = args[1]; // class name
83105
meta_args[1] = mp_obj_new_tuple(n_args - 2, args + 2); // tuple of bases
84-
meta_args[2] = class_locals; // dict of members
106+
meta_args[2] = class_locals; // dict of members (now from __prepare__)
85107
mp_obj_t new_class = mp_call_function_n_kw(meta, 3, 0, meta_args);
86108

87109
// store into cell if needed

py/mpconfig.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1314,6 +1314,14 @@ typedef time_t mp_timestamp_t;
13141314
#define MICROPY_PY_METACLASS_PROPERTIES (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
13151315
#endif
13161316

1317+
// Whether to support metaclass __prepare__ method (PEP 3115)
1318+
// Allows metaclasses to customize the namespace dict before class creation
1319+
// Required for enum.auto() to track insertion order
1320+
// Size impact: ~150-180 bytes
1321+
#ifndef MICROPY_PY_METACLASS_PREPARE
1322+
#define MICROPY_PY_METACLASS_PREPARE (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_FULL_FEATURES)
1323+
#endif
1324+
13171325
// Support for async/await/async for/async with
13181326
#ifndef MICROPY_PY_ASYNC_AWAIT
13191327
#define MICROPY_PY_ASYNC_AWAIT (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES)
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Test metaclass __prepare__ method (PEP 3115)
2+
3+
# Skip test if __prepare__ is not supported
4+
_prepare_test = []
5+
class _TestMeta(type):
6+
@classmethod
7+
def __prepare__(mcs, name, bases):
8+
_prepare_test.append(1)
9+
return {}
10+
11+
class _Test(metaclass=_TestMeta):
12+
pass
13+
14+
if not _prepare_test:
15+
print("SKIP")
16+
raise SystemExit
17+
18+
# Test 1: Basic __prepare__ is called before class body execution
19+
print("Test 1: Basic __prepare__ call")
20+
prepare_log = []
21+
22+
class Meta1(type):
23+
@classmethod
24+
def __prepare__(mcs, name, bases):
25+
prepare_log.append(f"__prepare__({name})")
26+
return {}
27+
28+
class Test1(metaclass=Meta1):
29+
prepare_log.append("body")
30+
31+
print(prepare_log)
32+
print("PASS" if prepare_log == ["__prepare__(Test1)", "body"] else "FAIL")
33+
34+
# Test 2: __prepare__ receives correct arguments
35+
print("\nTest 2: __prepare__ arguments")
36+
37+
class Meta2(type):
38+
@classmethod
39+
def __prepare__(mcs, name, bases):
40+
print(f"mcs={mcs.__name__}, name={name}, bases={bases}")
41+
return {}
42+
43+
class Base2:
44+
pass
45+
46+
class Test2(Base2, metaclass=Meta2):
47+
pass
48+
49+
# Test 3: __prepare__ return value is used as class namespace
50+
print("\nTest 3: __prepare__ return value as namespace")
51+
52+
class Meta3(type):
53+
@classmethod
54+
def __prepare__(mcs, name, bases):
55+
# Pre-populate namespace with a value
56+
return {"injected": 42}
57+
58+
class Test3(metaclass=Meta3):
59+
pass
60+
61+
print(f"Test3.injected = {Test3.injected}")
62+
print("PASS" if Test3.injected == 42 else "FAIL")
63+
64+
# Test 4: __prepare__ can access namespace in __new__
65+
print("\nTest 4: Access namespace from __new__")
66+
67+
class Meta4(type):
68+
@classmethod
69+
def __prepare__(mcs, name, bases):
70+
# Return dict with tracking info
71+
d = {}
72+
d['_prepared'] = True
73+
return d
74+
75+
def __new__(mcs, name, bases, namespace):
76+
# Verify __prepare__ returned dict was used
77+
was_prepared = namespace.get('_prepared', False)
78+
cls = type.__new__(mcs, name, bases, dict(namespace))
79+
cls._was_prepared = was_prepared
80+
return cls
81+
82+
class Test4(metaclass=Meta4):
83+
x = 1
84+
85+
print(f"Was prepared: {Test4._was_prepared}")
86+
print("PASS" if Test4._was_prepared else "FAIL")
87+
88+
# Test 5: __prepare__ inheritance
89+
print("\nTest 5: Inherited __prepare__")
90+
91+
class BaseMeta(type):
92+
@classmethod
93+
def __prepare__(mcs, name, bases):
94+
print(f"BaseMeta.__prepare__({name})")
95+
return {}
96+
97+
class DerivedMeta(BaseMeta):
98+
pass
99+
100+
class Test5(metaclass=DerivedMeta):
101+
pass
102+
103+
# Test 6: __prepare__ not called when metaclass doesn't define it
104+
print("\nTest 6: No __prepare__ defined")
105+
106+
class Meta6(type):
107+
pass
108+
109+
class Test6(metaclass=Meta6):
110+
x = 1
111+
112+
print(f"Test6.x = {Test6.x}")
113+
print("PASS")
114+
115+
print("\nAll __prepare__ tests completed!")
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
Test 1: Basic __prepare__ call
2+
['__prepare__(Test1)', 'body']
3+
PASS
4+
5+
Test 2: __prepare__ arguments
6+
mcs=Meta2, name=Test2, bases=(<class 'Base2'>,)
7+
8+
Test 3: __prepare__ return value as namespace
9+
Test3.injected = 42
10+
PASS
11+
12+
Test 4: Access namespace from __new__
13+
Was prepared: True
14+
PASS
15+
16+
Test 5: Inherited __prepare__
17+
BaseMeta.__prepare__(Test5)
18+
19+
Test 6: No __prepare__ defined
20+
Test6.x = 1
21+
PASS
22+
23+
All __prepare__ tests completed!

0 commit comments

Comments
 (0)