py/objfun: Implement function.__code__ and function constructor.

This allows retrieving the code object of a function using
`function.__code__`, and then reconstructing a function from a code object
using `FunctionType(code_object)`.

This feature is controlled by `MICROPY_PY_FUNCTION_ATTRS_CODE` and is
enabled at the full-features level.

Signed-off-by: Damien George <damien@micropython.org>
This commit is contained in:
Damien George
2025-01-20 22:23:48 +11:00
parent 62e821ccb8
commit ceb8ba60b4
8 changed files with 133 additions and 5 deletions

View File

@@ -1041,6 +1041,11 @@ typedef double mp_float_t;
#define MICROPY_PY_FUNCTION_ATTRS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
#endif
// Whether to implement the __code__ attribute on functions, and function constructor
#ifndef MICROPY_PY_FUNCTION_ATTRS_CODE
#define MICROPY_PY_FUNCTION_ATTRS_CODE (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_FULL_FEATURES)
#endif
// Whether to support the descriptors __get__, __set__, __delete__
// This costs some code size and makes load/store/delete of instance
// attributes slower for the classes that use this feature
@@ -1135,7 +1140,7 @@ typedef double mp_float_t;
#define MICROPY_PY_BUILTINS_CODE_BASIC (2)
#define MICROPY_PY_BUILTINS_CODE_FULL (3)
#ifndef MICROPY_PY_BUILTINS_CODE
#define MICROPY_PY_BUILTINS_CODE (MICROPY_PY_SYS_SETTRACE ? MICROPY_PY_BUILTINS_CODE_FULL : (MICROPY_PY_BUILTINS_COMPILE ? MICROPY_PY_BUILTINS_CODE_MINIMUM : MICROPY_PY_BUILTINS_CODE_NONE))
#define MICROPY_PY_BUILTINS_CODE (MICROPY_PY_SYS_SETTRACE ? MICROPY_PY_BUILTINS_CODE_FULL : (MICROPY_PY_FUNCTION_ATTRS_CODE ? MICROPY_PY_BUILTINS_CODE_BASIC : (MICROPY_PY_BUILTINS_COMPILE ? MICROPY_PY_BUILTINS_CODE_MINIMUM : MICROPY_PY_BUILTINS_CODE_NONE)))
#endif
// Whether to support dict.fromkeys() class method

View File

@@ -28,6 +28,8 @@
#include <string.h>
#include <assert.h>
#include "py/emitglue.h"
#include "py/objcode.h"
#include "py/objtuple.h"
#include "py/objfun.h"
#include "py/runtime.h"
@@ -151,6 +153,30 @@ qstr mp_obj_fun_get_name(mp_const_obj_t fun_in) {
return name;
}
#if MICROPY_PY_FUNCTION_ATTRS_CODE
static mp_obj_t fun_bc_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
(void)type;
mp_arg_check_num(n_args, n_kw, 2, 2, false);
if (!mp_obj_is_type(args[0], &mp_type_code)) {
mp_raise_TypeError(NULL);
}
if (!mp_obj_is_type(args[1], &mp_type_dict)) {
mp_raise_TypeError(NULL);
}
mp_obj_code_t *code = MP_OBJ_TO_PTR(args[0]);
mp_obj_t globals = args[1];
mp_module_context_t *module_context = m_new_obj(mp_module_context_t);
module_context->module.base.type = &mp_type_module;
module_context->module.globals = MP_OBJ_TO_PTR(globals);
module_context->constants = *mp_code_get_constants(code);
return mp_make_function_from_proto_fun(mp_code_get_proto_fun(code), module_context, NULL);
}
#endif
#if MICROPY_CPYTHON_COMPAT
static void fun_bc_print(const mp_print_t *print, mp_obj_t o_in, mp_print_kind_t kind) {
(void)kind;
@@ -340,9 +366,29 @@ void mp_obj_fun_bc_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
mp_obj_fun_bc_t *self = MP_OBJ_TO_PTR(self_in);
dest[0] = MP_OBJ_FROM_PTR(self->context->module.globals);
}
#if MICROPY_PY_FUNCTION_ATTRS_CODE
if (attr == MP_QSTR___code__) {
const mp_obj_fun_bc_t *self = MP_OBJ_TO_PTR(self_in);
if ((self->base.type == &mp_type_fun_bc
|| self->base.type == &mp_type_gen_wrap)
&& self->child_table == NULL) {
#if MICROPY_PY_BUILTINS_CODE <= MICROPY_PY_BUILTINS_CODE_BASIC
dest[0] = mp_obj_new_code(self->context->constants, self->bytecode);
#else
dest[0] = mp_obj_new_code(self->context, self->rc, true);
#endif
}
}
#endif
}
#endif
#if MICROPY_PY_FUNCTION_ATTRS_CODE
#define FUN_BC_MAKE_NEW make_new, fun_bc_make_new,
#else
#define FUN_BC_MAKE_NEW
#endif
#if MICROPY_CPYTHON_COMPAT
#define FUN_BC_TYPE_PRINT print, fun_bc_print,
#else
@@ -359,6 +405,7 @@ MP_DEFINE_CONST_OBJ_TYPE(
mp_type_fun_bc,
MP_QSTR_function,
MP_TYPE_FLAG_BINDS_SELF,
FUN_BC_MAKE_NEW
FUN_BC_TYPE_PRINT
FUN_BC_TYPE_ATTR
call, fun_bc_call

36
tests/basics/fun_code.py Normal file
View File

@@ -0,0 +1,36 @@
# Test function.__code__ attribute.
try:
(lambda: 0).__code__
except AttributeError:
print("SKIP")
raise SystemExit
def f():
return a
ftype = type(f)
# Test __code__ access and function constructor.
code = f.__code__
print(type(ftype(code, {})) is ftype)
# Test instantiating multiple code's with different globals dicts.
code = f.__code__
f1 = ftype(code, {"a": 1})
f2 = ftype(code, {"a": 2})
print(f1(), f2())
# Test bad first argument type.
try:
ftype(None, {})
except TypeError:
print("TypeError")
# Test bad second argument type.
try:
ftype(f.__code__, None)
except TypeError:
print("TypeError")

View File

@@ -0,0 +1,19 @@
# Test MicroPython-specific restrictions of function.__code__ attribute.
try:
(lambda: 0).__code__
except AttributeError:
print("SKIP")
raise SystemExit
def f_with_children():
def g():
pass
# Can't access __code__ when function has children.
try:
f_with_children.__code__
except AttributeError:
print("AttributeError")

View File

@@ -0,0 +1 @@
AttributeError

View File

@@ -21,11 +21,9 @@ print(a + [20, 30, 40])
# TODO: Faults
#print(a + a)
def foo():
print("hello from foo")
# subclassing a type that doesn't have make_new at the C level (not allowed)
try:
class myfunc(type(foo)):
class myfunc(type([].append)):
pass
except TypeError:
print("TypeError")

View File

@@ -0,0 +1,21 @@
# Test MicroPython-specific restrictions of function.__code__ attribute.
try:
(lambda: 0).__code__
except AttributeError:
print("SKIP")
raise SystemExit
import micropython
@micropython.native
def f_native():
pass
# Can't access __code__ when function is native code.
try:
f_native.__code__
except AttributeError:
print("AttributeError")

View File

@@ -0,0 +1 @@
AttributeError