diff --git a/Makefile b/Makefile index 931d9c1d2ca72..a164cab00e136 100644 --- a/Makefile +++ b/Makefile @@ -85,6 +85,7 @@ help: @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " fetch-all-submodules to fetch submodules for all ports" @echo " remove-all-submodules remove all submodules, including files and .git/ data" + @echo " fetch-tags fetch all tags for circuitpython from the adafruit circuitpython github repo" clean: rm -rf $(BUILDDIR)/* diff --git a/docs/library/micropython.rst b/docs/library/micropython.rst index 166192de3a38e..121359c858c25 100644 --- a/docs/library/micropython.rst +++ b/docs/library/micropython.rst @@ -26,3 +26,123 @@ Functions provided as part of the :mod:`micropython` module mainly so that scripts can be written which run under both CPython and MicroPython, by following the above pattern. + +.. function:: opt_level([level]) + + If *level* is given then this function sets the optimisation level for subsequent + compilation of scripts, and returns ``None``. Otherwise it returns the current + optimisation level. + + The optimisation level controls the following compilation features: + + - Assertions: at level 0 assertion statements are enabled and compiled into the + bytecode; at levels 1 and higher assertions are not compiled. + - Built-in ``__debug__`` variable: at level 0 this variable expands to ``True``; + at levels 1 and higher it expands to ``False``. + - Source-code line numbers: at levels 0, 1 and 2 source-code line number are + stored along with the bytecode so that exceptions can report the line number + they occurred at; at levels 3 and higher line numbers are not stored. + + The default optimisation level is usually level 0. + +.. function:: mem_info([verbose]) + + Print information about currently used memory. If the *verbose* argument + is given then extra information is printed. + + The information that is printed is implementation dependent, but currently + includes the amount of stack and heap used. In verbose mode it prints out + the entire heap indicating which blocks are used and which are free. + +.. function:: qstr_info([verbose]) + + Print information about currently interned strings. If the *verbose* + argument is given then extra information is printed. + + The information that is printed is implementation dependent, but currently + includes the number of interned strings and the amount of RAM they use. In + verbose mode it prints out the names of all RAM-interned strings. + +.. function:: stack_use() + + Return an integer representing the current amount of stack that is being + used. The absolute value of this is not particularly useful, rather it + should be used to compute differences in stack usage at different points. + +.. function:: heap_lock() +.. function:: heap_unlock() +.. function:: heap_locked() + + Lock or unlock the heap. When locked no memory allocation can occur and a + ``MemoryError`` will be raised if any heap allocation is attempted. + `heap_locked()` returns a true value if the heap is currently locked. + + These functions can be nested, ie `heap_lock()` can be called multiple times + in a row and the lock-depth will increase, and then `heap_unlock()` must be + called the same number of times to make the heap available again. + + Both `heap_unlock()` and `heap_locked()` return the current lock depth + (after unlocking for the former) as a non-negative integer, with 0 meaning + the heap is not locked. + + If the REPL becomes active with the heap locked then it will be forcefully + unlocked. + + Note: `heap_locked()` is not enabled on most ports by default, + requires ``MICROPY_PY_MICROPYTHON_HEAP_LOCKED``. + +.. function:: kbd_intr(chr) + + Set the character that will raise a `KeyboardInterrupt` exception. By + default this is set to 3 during script execution, corresponding to Ctrl-C. + Passing -1 to this function will disable capture of Ctrl-C, and passing 3 + will restore it. + + This function can be used to prevent the capturing of Ctrl-C on the + incoming stream of characters that is usually used for the REPL, in case + that stream is used for other purposes. + +.. function:: schedule(func, arg) + + Schedule the function *func* to be executed "very soon". The function + is passed the value *arg* as its single argument. "Very soon" means that + the MicroPython runtime will do its best to execute the function at the + earliest possible time, given that it is also trying to be efficient, and + that the following conditions hold: + + - A scheduled function will never preempt another scheduled function. + - Scheduled functions are always executed "between opcodes" which means + that all fundamental Python operations (such as appending to a list) + are guaranteed to be atomic. + - A given port may define "critical regions" within which scheduled + functions will never be executed. Functions may be scheduled within + a critical region but they will not be executed until that region + is exited. An example of a critical region is a preempting interrupt + handler (an IRQ). + + A use for this function is to schedule a callback from a preempting IRQ. + Such an IRQ puts restrictions on the code that runs in the IRQ (for example + the heap may be locked) and scheduling a function to call later will lift + those restrictions. + + Note: If `schedule()` is called from a preempting IRQ, when memory + allocation is not allowed and the callback to be passed to `schedule()` is + a bound method, passing this directly will fail. This is because creating a + reference to a bound method causes memory allocation. A solution is to + create a reference to the method in the class constructor and to pass that + reference to `schedule()`. + + There is a finite queue to hold the scheduled functions and `schedule()` + will raise a `RuntimeError` if the queue is full. + + +Constants +--------- + +.. data:: TYPE_CHECKING + + A special constant that is typed as ``True`` for type checkers but will + always evaluate as ``false`` at runtime. This serves the same purpose as + `TYPE_CHECKING `_ + in cpython but does not involve the use of the ``typing`` module (as it is not + guaranteed on circuipython). \ No newline at end of file diff --git a/mpy-cross/mpconfigport.h b/mpy-cross/mpconfigport.h index e0772470110a3..0ed58fcb26355 100644 --- a/mpy-cross/mpconfigport.h +++ b/mpy-cross/mpconfigport.h @@ -93,6 +93,8 @@ #define MICROPY_PY_IO (0) #define MICROPY_PY_SYS (0) +#define MICROPY_COMP_FALSE_TYPE_CHECKING_LITERAL (1) + // MINGW only handles these errno names. #ifdef __MINGW32__ #define MICROPY_PY_ERRNO_LIST \ diff --git a/py/circuitpy_mpconfig.h b/py/circuitpy_mpconfig.h index 8fc707aca49ce..90abeaa56c8e9 100644 --- a/py/circuitpy_mpconfig.h +++ b/py/circuitpy_mpconfig.h @@ -322,7 +322,7 @@ typedef long mp_off_t; #if defined(DEFAULT_UART_BUS_TX) && defined(DEFAULT_UART_BUS_RX) #define CIRCUITPY_BOARD_UART (1) #define CIRCUITPY_BOARD_UART_PIN {{.tx = DEFAULT_UART_BUS_TX, .rx = DEFAULT_UART_BUS_RX}} -#else +#else // TODO(add checks here for if only one is defined and #error if so) #define CIRCUITPY_BOARD_UART (0) #endif #endif @@ -462,6 +462,10 @@ void background_callback_run_all(void); #define CIRCUITPY_STATUS_LED_POWER_INVERTED (0) #endif +#ifndef MICROPY_COMP_FALSE_TYPE_CHECKING_LITERAL +#define MICROPY_COMP_FALSE_TYPE_CHECKING_LITERAL (1) +#endif + #define CIRCUITPY_BOOT_OUTPUT_FILE "/boot_out.txt" #ifndef CIRCUITPY_BOOT_COUNTER diff --git a/py/makeversionhdr.py b/py/makeversionhdr.py index 430b9bef4be51..788dc12d0af61 100644 --- a/py/makeversionhdr.py +++ b/py/makeversionhdr.py @@ -13,6 +13,8 @@ import datetime import subprocess +assert sys.version_info >= (3, 7), f"Python 3.7 or newer is required. this was run on {sys.version} at {repr(sys.executable)}" + # CIRCUITPY-CHANGE: use external script that can override git describe output with an # environment variable. tools_describe = str(pathlib.Path(__file__).resolve().parent.parent / "tools/describe") @@ -23,8 +25,8 @@ def get_version_info_from_git(repo_path): try: subprocess.check_output subprocess.check_call - except AttributeError: - return None + except AttributeError as err: + return err # Note: git describe doesn't work if no tag is available try: @@ -38,10 +40,11 @@ def get_version_info_from_git(repo_path): except subprocess.CalledProcessError as er: if er.returncode == 128: # git exit code of 128 means no repository found - return None + return er + raise ValueError(f"git describe failed with {er.returncode}: {er.output}") git_tag = "" - except OSError: - return None + except OSError as err: + return err try: git_hash = subprocess.check_output( ["git", "rev-parse", "--short", "HEAD"], @@ -51,8 +54,8 @@ def get_version_info_from_git(repo_path): ).strip() except subprocess.CalledProcessError: git_hash = "unknown" - except OSError: - return None + except OSError as err: + return err try: # Check if there are any modified files. @@ -69,13 +72,16 @@ def get_version_info_from_git(repo_path): ) except subprocess.CalledProcessError: git_hash += "-dirty" - except OSError: - return None + except OSError as err: + return err # CIRCUITPY-CHANGE # Try to extract MicroPython version from git tag ver = git_tag.split("-")[0].split(".") + if ver < ["9", "0", "0"]: + raise Exception("The detected version is out of date", git_tag, git_hash, ver, git_tag) + return git_tag, git_hash, ver @@ -95,8 +101,12 @@ def make_version_header(repo_path, filename): info = get_version_info_from_git(repo_path) if info is None: cannot_determine_version() + elif isinstance(info, Exception): + # raise info + cannot_determine_version() git_tag, git_hash, ver = info if len(ver) < 3: + raise ValueError(f"Invalid version info {info}") cannot_determine_version() else: version_string = ".".join(ver) diff --git a/py/modmicropython.c b/py/modmicropython.c index 7c097ff48701d..c0d21da2f65bd 100644 --- a/py/modmicropython.c +++ b/py/modmicropython.c @@ -169,6 +169,7 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_2(mp_micropython_schedule_obj, mp_micropython_sch STATIC const mp_rom_map_elem_t mp_module_micropython_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_micropython) }, { MP_ROM_QSTR(MP_QSTR_const), MP_ROM_PTR(&mp_identity_obj) }, + { MP_ROM_QSTR(MP_QSTR_TYPE_CHECKING), MP_ROM_PTR(mp_const_false) }, #if CIRCUITPY_MICROPYTHON_ADVANCED && MICROPY_ENABLE_COMPILER { MP_ROM_QSTR(MP_QSTR_opt_level), MP_ROM_PTR(&mp_micropython_opt_level_obj) }, #endif diff --git a/py/mpconfig.h b/py/mpconfig.h index 195eb5981ab71..5ec35bcbac530 100644 --- a/py/mpconfig.h +++ b/py/mpconfig.h @@ -483,6 +483,13 @@ #define MICROPY_COMP_RETURN_IF_EXPR (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES) #endif +// whether to replace `TYPE_CHECKING` with `0` in expressions +// adds 36 bytes to the parser (Thumb2) and prevents emitting blocks of code that will never run +#ifndef MICROPY_COMP_FALSE_TYPE_CHECKING_LITERAL +#define MICROPY_COMP_FALSE_TYPE_CHECKING_LITERAL (0) +#endif + + /*****************************************************************************/ /* Internal debugging stuff */ @@ -1275,6 +1282,23 @@ typedef double mp_float_t; #define MICROPY_PY_BUILTINS_HELP_MODULES (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES) #endif +// Whether to include __class_getitem__ support as per pep 560, cpython 3.7+ +// This is the part of python typing that is not in type annotations (ie not ignored) +#ifndef MICROPY_PY_TYPE_CLASS_GETITEM +#define MICROPY_PY_TYPE_CLASS_GETITEM (MICROPY_CPYTHON_COMPAT) +#endif + +// Whether to call __set_name__ and __init_subclass__ when creating classes (see PEP ) +#ifndef MICROPY_PY_ENABLE_TYPE_CLASS_CREATION_HOOKS +#define MICROPY_PY_ENABLE_TYPE_CLASS_CREATION_HOOKS (MICROPY_CPYTHON_COMPAT) +#endif + +// Whether to include type[...] for builtin classes as per pep 585, cpython 3.9+ +// Set off b/c pep 585's primary usecase is in type hints, which micropython does not evaluate +#ifndef MICROPY_PY_TYPE_GENERIC_BUILTINS +#define MICROPY_PY_TYPE_GENERIC_BUILTINS (MICROPY_CPYTHON_COMPAT) +#endif + // Whether to set __file__ for imported modules #ifndef MICROPY_PY___FILE__ #define MICROPY_PY___FILE__ (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES) diff --git a/py/objtype.c b/py/objtype.c index b9d83f9c85ce9..9e39f92518321 100644 --- a/py/objtype.c +++ b/py/objtype.c @@ -1130,6 +1130,58 @@ STATIC void type_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { } } +#if MICROPY_PY_TYPE_CLASS_GETITEM || MICROPY_PY_TYPE_GENERIC_BUILTINS +STATIC mp_obj_t type_cls_getitem_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) { + mp_obj_type_t *self = MP_OBJ_TO_PTR(self_in); + + #if MICROPY_PY_TYPE_GENERIC_BUILTINS + // check for built-in types as per supported types in pep 585 + // however, here we only return the type itself, not a wrapper around it + // This includes the `type` type and the other container types in builtins: dict, frozenset, list, set, tuple + if ( self == &mp_type_type + || self == &mp_type_dict + || self == &mp_type_list + || self == &mp_type_tuple + #if MICROPY_PY_BUILTINS_SET + || self == &mp_type_set + #endif + #if MICROPY_PY_BUILTINS_FROZENSET + || self == &mp_type_frozenset + #endif + #if MICROPY_PY_COLLECTIONS + #if MICROPY_PY_COLLECTIONS_ORDEREDDICT + || self == &mp_type_ordereddict + #endif + #if MICROPY_PY_COLLECTIONS_DEQUE + || self == &mp_type_deque + #endif + #endif + ) { + return self; + } + #endif // if MICROPY_PY_TYPE_GENERIC_BUILTINS + + #if MICROPY_PY_TYPE_CLASS_GETITEM + // lookup __class_getitem__ in the class heirarchy + mp_obj_t class_getitem_func = MP_OBJ_NULL; + struct class_lookup_data lookup = { + .obj = (mp_obj_instance_t *)self, + .attr = MP_QSTR___class_getitem__, + .slot_offset = 0, + .dest = &class_getitem_func, + .is_type = true, + }; + mp_obj_class_lookup(&lookup, self); + if (class_getitem_func != MP_OBJ_NULL) { + return mp_call_function_2(class_getitem_func, self, index); + } + #endif // if MICROPY_PY_TYPE_CLASS_GETITEM + // else raise normal + return MP_OBJ_NULL; +} +#endif + + MP_DEFINE_CONST_OBJ_TYPE( mp_type_type, MP_QSTR_type, @@ -1137,9 +1189,64 @@ MP_DEFINE_CONST_OBJ_TYPE( make_new, type_make_new, print, type_print, call, type_call, +#if MICROPY_PY_TYPE_CLASS_GETITEM || MICROPY_PY_TYPE_GENERIC_BUILTINS + subscr, type_cls_getitem_subscr, +#endif attr, type_attr ); + +STATIC void super_class_lookup(mp_obj_t obj, qstr attr, mp_obj_t* dest) { + + if (dest[0] != MP_OBJ_NULL) { + // not load attribute + return; + } + + assert(mp_obj_is_type(obj, &mp_type_type)); + + mp_obj_type_t *type = MP_OBJ_TO_PTR(obj); + + struct class_lookup_data lookup = { + .obj = MP_OBJ_TO_PTR(obj), + .attr = attr, + .slot_offset = 0, + .dest = dest, + .is_type = false, + }; + + // Allow a call super().__init__() to reach any native base classes + if (attr == MP_QSTR___init__) { + lookup.slot_offset = MP_OBJ_TYPE_OFFSETOF_SLOT(make_new); + } + + const void *parent = MP_OBJ_TYPE_GET_SLOT_OR_NULL(type, parent); + if (parent == NULL) { + // no parents, do nothing + #if MICROPY_MULTIPLE_INHERITANCE + } else if (((mp_obj_base_t *)parent)->type == &mp_type_tuple) { + const mp_obj_tuple_t *parent_tuple = parent; + size_t len = parent_tuple->len; + const mp_obj_t *items = parent_tuple->items; + for (size_t i = 0; i < len; i++) { + assert(mp_obj_is_type(items[i], &mp_type_type)); + if (MP_OBJ_TO_PTR(items[i]) == &mp_type_object) { + // The "object" type will be searched at the end of this function, + // and we don't want to lookup native methods in object. + continue; + } + mp_obj_class_lookup(&lookup, (mp_obj_type_t *)MP_OBJ_TO_PTR(items[i])); + if (dest[0] != MP_OBJ_NULL) { + break; + } + } + #endif + } else if (parent != &mp_type_object) { + mp_obj_class_lookup(&lookup, parent); + } +} + + mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict) { // Verify input objects have expected type if (!mp_obj_is_type(bases_tuple, &mp_type_tuple)) { @@ -1260,7 +1367,37 @@ mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict) } } - return MP_OBJ_FROM_PTR(o); + mp_obj_t as_obj = MP_OBJ_FROM_PTR(o); + + #if MICROPY_PY_ENABLE_TYPE_CLASS_CREATION_HOOKS + // implement PEP 487, https://peps.python.org/pep-0487/ + + // find and call the .__set_name__ method on each of objects in the locals dict of the type object + mp_obj_dict_t *const ld = MP_OBJ_TYPE_GET_SLOT_OR_NULL(o, locals_dict); + if (ld != NULL) + for (size_t i = 0; i < ld->map.alloc ; i++) { + if (mp_map_slot_is_filled(&ld->map, i)) { + mp_map_elem_t *const this_elem = &ld->map.table[i]; + mp_obj_t dest2[4] = {MP_OBJ_NULL}; + mp_load_method_maybe(this_elem->value, MP_QSTR___set_name__, dest2); + if (dest2[0] != MP_OBJ_NULL) { + // call __set_name__, reuse dest as the args array + dest2[2] = as_obj; + dest2[3] = this_elem->key; + mp_call_method_n_kw(2, 0, dest2); + } + } + } + + // check for and call __init_subclass__ + mp_obj_t dest[2] = {MP_OBJ_NULL, MP_OBJ_NULL}; + super_class_lookup(as_obj, MP_QSTR___init_subclass__, dest); + if (dest[0] != MP_OBJ_NULL){ + mp_call_function_1(dest[0], as_obj); + } + #endif + + return as_obj; } /******************************************************************************/ diff --git a/py/parse.c b/py/parse.c index a430f82578937..a1e3b8571341a 100644 --- a/py/parse.c +++ b/py/parse.c @@ -592,6 +592,16 @@ STATIC void push_result_token(parser_t *parser, uint8_t rule_id) { #if MICROPY_COMP_CONST // if name is a standalone identifier, look it up in the table of dynamic constants mp_map_elem_t *elem; + + #if MICROPY_COMP_FALSE_TYPE_CHECKING_LITERAL + // special case for the `TYPE_CHECKING` atom to replace it with `False` parse node + // (which is "redefining" it as a macro/keyword, kinds) + if (rule_id == RULE_atom && id == MP_QSTR_TYPE_CHECKING) { + // TODO(only apply TYPE_CHECKING optimiziation if it has been imported from the typing module) + pn = make_node_const_object_optimised(parser, lex->tok_line, mp_const_false); + } // the else below this line joins with the following if to form an `} else if {` + else // intentionally does not have a closing brace + #endif if (rule_id == RULE_atom && (elem = mp_map_lookup(&parser->consts, MP_OBJ_NEW_QSTR(id), MP_MAP_LOOKUP)) != NULL) { pn = make_node_const_object_optimised(parser, lex->tok_line, elem->value); diff --git a/py/profile.c b/py/profile.c index 274089d7022f1..7dddddfeb24ae 100644 --- a/py/profile.c +++ b/py/profile.c @@ -183,7 +183,8 @@ MP_DEFINE_CONST_OBJ_TYPE( MP_QSTR_code, MP_TYPE_FLAG_NONE, print, code_print, - attr, code_attr + attr, code_attr, + unary_op, mp_generic_unary_op ); mp_obj_t mp_obj_new_code(const mp_module_context_t *context, const mp_raw_code_t *rc) { @@ -247,14 +248,17 @@ STATIC void frame_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) { } } + MP_DEFINE_CONST_OBJ_TYPE( mp_type_frame, MP_QSTR_frame, MP_TYPE_FLAG_NONE, print, frame_print, - attr, frame_attr + attr, frame_attr, + unary_op, mp_generic_unary_op ); + mp_obj_t mp_obj_new_frame(const mp_code_state_t *code_state) { if (gc_is_locked()) { return MP_OBJ_NULL;