diff --git a/examples/usercmodule/cexample/examplemodule.c b/examples/usercmodule/cexample/examplemodule.c index b2457b28011af..076a412c28cd9 100644 --- a/examples/usercmodule/cexample/examplemodule.c +++ b/examples/usercmodule/cexample/examplemodule.c @@ -69,6 +69,88 @@ MP_DEFINE_CONST_OBJ_TYPE( locals_dict, &example_Timer_locals_dict ); + +// What follows is a *separate* class definition that demonstrates more +// advanced techniques to implement other Python-like features, such as: +// +// - A custom representation for __repr__ and __str__. +// - Custom attribute handling to create a read/write "property". +// +// It re-uses some of the elements of the basic Timer class. This is allowed +// because they both use example_Timer_obj_t as the instance structure. + +// Handles AdvancedTimer.__repr__, AdvancedTimer.__str__. +STATIC void example_AdvancedTimer_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + + // Get the elapsed time. In this case, it's also a demonstration of calling + // the equivalent of self.time() in the C API. This is not usually very + // efficient, but it can sometimes be useful. + mp_uint_t elapsed = mp_obj_get_int(example_Timer_time(self_in)); + + // We'll make all representations print at least the class name. + mp_printf(print, "%q()", MP_QSTR_AdvancedTimer); + + // Decide what else to print based on print kind. + if (kind == PRINT_STR) { + // For __str__, let's attempt to make it more readable. + mp_printf(print, " # created %d seconds ago", elapsed / 1000); + } +} + +// Handles AdvancedTimer.seconds for reading and writing. +STATIC void example_AdvancedTimer_attribute_handler(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { + + // In this example, we only want to handle the .seconds attribute in a + // special way. + if (attr != MP_QSTR_seconds) { + // Attribute not found, continue lookup in locals dict. This way, + // methods like .time() will be handled normally. + dest[1] = MP_OBJ_SENTINEL; + return; + } + + // Get reference to AdvancedTimer instance. + example_Timer_obj_t *self = MP_OBJ_TO_PTR(self_in); + + // Check if this is a read operation. + if (dest[0] == MP_OBJ_NULL) { + // It's read, so "return" elapsed seconds by storing it in dest[0]. + mp_uint_t elapsed = mp_hal_ticks_ms() - self->start_time; + dest[0] = mp_obj_new_int_from_uint(elapsed / 1000); + return; + } + // Check if this is a delete or store operation. + else if (dest[0] == MP_OBJ_SENTINEL) { + // It's delete or store. Now check which one. + if (dest[1] == MP_OBJ_NULL) { + // It's delete. But in this example we don't want to allow it + // so we just return. + return; + } else { + // It's write. First, get the value that the user is trying to set. + mp_uint_t desired_ms = mp_obj_get_int(dest[1]) * 1000; + // Use it to update the start time. This way, the next read will + // report the updated time. + self->start_time = mp_hal_ticks_ms() - desired_ms; + + // Indicate successful store. + dest[0] = MP_OBJ_NULL; + return; + } + } +} + +// This defines the type(AdvancedTimer) object. +MP_DEFINE_CONST_OBJ_TYPE( + example_type_AdvancedTimer, + MP_QSTR_AdvancedTimer, + MP_TYPE_FLAG_NONE, + attr, example_AdvancedTimer_attribute_handler, + print, example_AdvancedTimer_print, + make_new, example_Timer_make_new, + locals_dict, &example_Timer_locals_dict + ); + // Define all properties of the module. // Table entries are key/value pairs of the attribute name (a string) // and the MicroPython object reference. @@ -78,6 +160,7 @@ STATIC const mp_rom_map_elem_t example_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_cexample) }, { MP_ROM_QSTR(MP_QSTR_add_ints), MP_ROM_PTR(&example_add_ints_obj) }, { MP_ROM_QSTR(MP_QSTR_Timer), MP_ROM_PTR(&example_type_Timer) }, + { MP_ROM_QSTR(MP_QSTR_AdvancedTimer), MP_ROM_PTR(&example_type_AdvancedTimer) }, }; STATIC MP_DEFINE_CONST_DICT(example_module_globals, example_module_globals_table); diff --git a/py/objtype.c b/py/objtype.c index 909fc833931f6..a202b023bcf03 100644 --- a/py/objtype.c +++ b/py/objtype.c @@ -82,7 +82,7 @@ STATIC int instance_count_native_bases(const mp_obj_type_t *type, const mp_obj_t } } -// This wrapper function is allows a subclass of a native type to call the +// This wrapper function allows a subclass of a native type to call the // __init__() method (corresponding to type->make_new) of the native type. STATIC mp_obj_t native_base_init_wrapper(size_t n_args, const mp_obj_t *args) { mp_obj_instance_t *self = MP_OBJ_TO_PTR(args[0]); @@ -170,6 +170,12 @@ STATIC void mp_obj_class_lookup(struct class_lookup_data *lookup, const mp_obj_t if (obj != NULL && mp_obj_is_native_type(type) && type != &mp_type_object /* object is not a real type */) { // If we're dealing with native base class, then it applies to native sub-object obj_obj = obj->subobj[0]; + #if MICROPY_BUILTIN_METHOD_CHECK_SELF_ARG + if (obj_obj == MP_OBJ_FROM_PTR(&native_base_init_wrapper_obj)) { + // But we shouldn't attempt lookups on object that is not yet instantiated. + mp_raise_msg(&mp_type_AttributeError, MP_ERROR_TEXT("call super().__init__() first")); + } + #endif // MICROPY_BUILTIN_METHOD_CHECK_SELF_ARG } else { obj_obj = MP_OBJ_FROM_PTR(obj); } diff --git a/tests/cpydiff/core_class_super_init.py b/tests/cpydiff/core_class_super_init.py new file mode 100644 index 0000000000000..1774f61dd82e4 --- /dev/null +++ b/tests/cpydiff/core_class_super_init.py @@ -0,0 +1,31 @@ +""" +categories: Core,Classes +description: When inheriting native types, calling a method in ``__init__(self, ...)`` before ``super().__init__()`` raises an ``AttributeError`` (or segfaults if ``MICROPY_BUILTIN_METHOD_CHECK_SELF_ARG`` is not enabled). +cause: MicroPython does not have separate ``__new__`` and ``__init__`` methods in native types. +workaround: Call ``super().__init__()`` first. +""" + + +class L1(list): + def __init__(self, a): + self.append(a) + + +try: + L1(1) + print("OK") +except AttributeError: + print("AttributeError") + + +class L2(list): + def __init__(self, a): + super().__init__() + self.append(a) + + +try: + L2(1) + print("OK") +except AttributeError: + print("AttributeError") diff --git a/tests/misc/cexample_class.py b/tests/misc/cexample_class.py index 6b8718ad8cc5f..06d741922d289 100644 --- a/tests/misc/cexample_class.py +++ b/tests/misc/cexample_class.py @@ -22,3 +22,20 @@ print(timer) print(0 <= t_start <= TOLERANCE_MS) print(SLEEP_MS - TOLERANCE_MS <= t_end <= SLEEP_MS + TOLERANCE_MS) + +advanced_timer = cexample.AdvancedTimer() + +time.sleep_ms(100) + +print(repr(advanced_timer)) +print(str(advanced_timer)) + +print(advanced_timer.seconds) +advanced_timer.seconds = 123 +print(advanced_timer.seconds) +print(advanced_timer.time() < 123000 + TOLERANCE_MS) + +try: + advanced_timer.seconds = "bad input" +except TypeError: + print("TypeError") diff --git a/tests/misc/cexample_class.py.exp b/tests/misc/cexample_class.py.exp index b9a06602a316a..a86d4d14f78df 100644 --- a/tests/misc/cexample_class.py.exp +++ b/tests/misc/cexample_class.py.exp @@ -1,3 +1,9 @@ True True +AdvancedTimer() +AdvancedTimer() # created 0 seconds ago +0 +123 +True +TypeError diff --git a/tests/misc/cexample_module.py b/tests/misc/cexample_module.py index c1da2ecf7ab24..979c1fa24b376 100644 --- a/tests/misc/cexample_module.py +++ b/tests/misc/cexample_module.py @@ -12,5 +12,6 @@ d = dir(cexample) d.index("add_ints") d.index("Timer") +d.index("AdvancedTimer") print(cexample.add_ints(1, 3)) diff --git a/tests/misc/cexample_subclass.py b/tests/misc/cexample_subclass.py new file mode 100644 index 0000000000000..8222a51c68d13 --- /dev/null +++ b/tests/misc/cexample_subclass.py @@ -0,0 +1,38 @@ +# test subclassing custom native class + +try: + from cexample import AdvancedTimer +except ImportError: + print("SKIP") + raise SystemExit + + +class SubTimer(AdvancedTimer): + def __init__(self): + + # At this point, self does not yet represent a AdvancedTimer instance. + print(self) + + # So lookups via type.attr handler will fail. + try: + self.seconds + except AttributeError: + print("AttributeError") + + # Also applies to builtin methods. + try: + self.time() + except AttributeError: + print("AttributeError") + + # Initialize base class. + super().__init__(self) + + # Now you can access methods and attributes normally. + self.time() + print(self.seconds) + self.seconds = 123 + print(self.seconds) + + +watch = SubTimer() diff --git a/tests/misc/cexample_subclass.py.exp b/tests/misc/cexample_subclass.py.exp new file mode 100644 index 0000000000000..a035649e475db --- /dev/null +++ b/tests/misc/cexample_subclass.py.exp @@ -0,0 +1,5 @@ + +AttributeError +AttributeError +0 +123