Skip to content
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

Customize attribute access for object #2761

Closed
wants to merge 2 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions py/mpconfig.h
Expand Up @@ -635,6 +635,17 @@ typedef double mp_float_t;
#define MICROPY_PY_DESCRIPTORS (0)
#endif

// Whether to support (__delattr__ and __setattr__)
// This costs some code size and makes all load attrs and store attrs slow
#ifndef MICROPY_PY_METHODS_DELATTRS_SETATTRS
#define MICROPY_PY_METHODS_DELATTRS_SETATTRS (0)
#endif

// Whether to support object (__delattr__, __getattribute__ and __setattr__)
#ifndef MICROPY_PY_OBJECT_METHODS_DELATTRS_SETATTRS
#define MICROPY_PY_OBJECT_METHODS_DELATTRS_SETATTRS (0)
#endif

// Support for async/await/async for/async with
#ifndef MICROPY_PY_ASYNC_AWAIT
#define MICROPY_PY_ASYNC_AWAIT (1)
Expand Down
40 changes: 40 additions & 0 deletions py/objobject.c
Expand Up @@ -59,13 +59,53 @@ STATIC mp_obj_t object___new__(mp_obj_t cls) {
STATIC MP_DEFINE_CONST_FUN_OBJ_1(object___new___fun_obj, object___new__);
STATIC MP_DEFINE_CONST_STATICMETHOD_OBJ(object___new___obj, MP_ROM_PTR(&object___new___fun_obj));

#if MICROPY_PY_OBJECT_METHODS_DELATTRS_SETATTRS
STATIC mp_obj_t object___setattr__(mp_obj_t self_in, mp_obj_t attr_in, mp_obj_t value) {
mp_obj_instance_t *self = MP_OBJ_TO_PTR(self_in);
mp_map_lookup(&self->members, MP_OBJ_NEW_QSTR(mp_obj_str_get_qstr(attr_in)), MP_MAP_LOOKUP_ADD_IF_NOT_FOUND)->value = value;
return value;
}
MP_DEFINE_CONST_FUN_OBJ_3(object___setattr___obj, object___setattr__);

STATIC mp_obj_t object___getattribute__(mp_obj_t self_in, mp_obj_t attr_in) {
mp_obj_instance_t *self = MP_OBJ_TO_PTR(self_in);
qstr attr = mp_obj_str_get_qstr(attr_in);
// check if exists into locals_dict and return its value
// if this fails to load the requested attr, we raise attribute error
mp_map_elem_t *elem = mp_map_lookup(&self->members, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP);
if (elem == NULL) {
nlr_raise(mp_obj_new_exception_msg_varg(&mp_type_AttributeError,
"'%s' object has no attribute '%q'",
mp_obj_get_type_str(self_in), attr));
}
return elem->value;
}
MP_DEFINE_CONST_FUN_OBJ_2(object___getattribute___obj, object___getattribute__);

STATIC mp_obj_t object___delattr__(mp_obj_t self_in, mp_obj_t attr_in) {
mp_obj_instance_t *self = MP_OBJ_TO_PTR(self_in);
mp_map_lookup(&self->members, MP_OBJ_NEW_QSTR(mp_obj_str_get_qstr(attr_in)), MP_MAP_LOOKUP_REMOVE_IF_FOUND);
return mp_const_none;
}
MP_DEFINE_CONST_FUN_OBJ_2(object___delattr___obj, object___delattr__);
#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) },
#endif
#if MICROPY_CPYTHON_COMPAT
{ MP_ROM_QSTR(MP_QSTR___new__), MP_ROM_PTR(&object___new___obj) },
#endif
#if (MICROPY_CPYTHON_COMPAT && MICROPY_PY_OBJECT_METHODS_DELATTRS_SETATTRS)
{ MP_ROM_QSTR(MP_QSTR___delattr__), MP_ROM_PTR(&object___delattr___obj) },
#endif
#if (MICROPY_CPYTHON_COMPAT && MICROPY_PY_OBJECT_METHODS_DELATTRS_SETATTRS)
{ MP_ROM_QSTR(MP_QSTR___getattribute__), MP_ROM_PTR(&object___getattribute___obj) },
#endif
#if (MICROPY_CPYTHON_COMPAT && MICROPY_PY_OBJECT_METHODS_DELATTRS_SETATTRS)
{ MP_ROM_QSTR(MP_QSTR___setattr__), MP_ROM_PTR(&object___setattr___obj) },
#endif
};

STATIC MP_DEFINE_CONST_DICT(object_locals_dict, object_locals_dict_table);
Expand Down
37 changes: 37 additions & 0 deletions py/objtype.c
Expand Up @@ -533,6 +533,14 @@ STATIC void mp_obj_instance_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *des

// try __getattr__
if (attr != MP_QSTR___getattr__) {
#if MICROPY_PY_METHODS_DELATTRS_SETATTRS
// if the requested attr is __setattr__ assign MP_OBJECT_NULL to dest[0] to indicate success
if (attr == MP_QSTR___setattr__) {
dest[0] = MP_OBJ_NULL;
return;
}
#endif

mp_obj_t dest2[3];
mp_load_method_maybe(self_in, MP_QSTR___getattr__, dest2);
if (dest2[0] != MP_OBJ_NULL) {
Expand Down Expand Up @@ -626,10 +634,39 @@ STATIC bool mp_obj_instance_store_attr(mp_obj_t self_in, qstr attr, mp_obj_t val

if (value == MP_OBJ_NULL) {
// delete attribute
#if MICROPY_PY_METHODS_DELATTRS_SETATTRS
// try __delattr__ first
if (attr != MP_QSTR___delattr__) {
mp_obj_t attr_delattr_method[3];
mp_load_method_maybe(self_in, MP_QSTR___delattr__, attr_delattr_method);
if (attr_delattr_method[0] != MP_OBJ_NULL) {
// __delattr__ exists, call it and return its result
attr_delattr_method[2] = MP_OBJ_NEW_QSTR(attr);
mp_call_method_n_kw(1, 0, attr_delattr_method);
return true;
}
}
#endif

mp_map_elem_t *elem = mp_map_lookup(&self->members, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP_REMOVE_IF_FOUND);
return elem != NULL;
} else {
// store attribute
#if MICROPY_PY_METHODS_DELATTRS_SETATTRS
// try __setattr__ first
if (attr != MP_QSTR___setattr__) {
mp_obj_t attr_setattr_method[4];
mp_load_method_maybe(self_in, MP_QSTR___setattr__, attr_setattr_method);
if (attr_setattr_method[0] != MP_OBJ_NULL) {
// __setattr__ exists, call it and return its result
attr_setattr_method[2] = MP_OBJ_NEW_QSTR(attr);
attr_setattr_method[3] = value;
mp_call_method_n_kw(2, 0, attr_setattr_method);
return true;
}
}
#endif

mp_map_lookup(&self->members, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP_ADD_IF_NOT_FOUND)->value = value;
return true;
}
Expand Down
6 changes: 5 additions & 1 deletion py/runtime.c
Expand Up @@ -932,7 +932,11 @@ STATIC mp_obj_t checked_fun_call(mp_obj_t self_in, size_t n_args, size_t n_kw, c
mp_obj_checked_fun_t *self = MP_OBJ_TO_PTR(self_in);
if (n_args > 0) {
const mp_obj_type_t *arg0_type = mp_obj_get_type(args[0]);
if (arg0_type != self->type) {
if (arg0_type != self->type
#if MICROPY_PY_OBJECT_METHODS_DELATTRS_SETATTRS
&& !MP_OBJ_IS_TYPE(arg0_type, &mp_type_type)
#endif
) {
if (MICROPY_ERROR_REPORTING != MICROPY_ERROR_REPORTING_DETAILED) {
mp_raise_msg(&mp_type_TypeError, "argument has wrong type");
} else {
Expand Down
27 changes: 27 additions & 0 deletions tests/basics/delsetattr.py
@@ -0,0 +1,27 @@
# test __delattr__ and __setattr__

class A:
def __init__(self, d):
A.d = d

def __getattr__(self, attr):
if attr in A.d:
return A.d[attr]
else:
raise AttributeError(attr)

def __setattr__(self, attr, value):
A.d[attr] = value

def __delattr__(self, attr):
del A.d[attr]

a = A({"a":1, "b":2})
print(a.a, a.b)
a.a = 3
try:
print(a.a, a.b)
del a.a
print(a.a)
except AttributeError:
print("AttributeError")
34 changes: 34 additions & 0 deletions tests/basics/object_delgetsetattr.py
@@ -0,0 +1,34 @@

class A:

def __init__(self, a=0, b=0, c=0, d=b''):
self._a = a
self._b = b
self._c = c
self._d = d

def __getattr__(self, name):
if not name.startswith("_"):
name = "_" + name
return object.__getattribute__(self, name)

def __setattr__(self, name, value):
if not name.startswith("_"):
name = "_" + name
object.__setattr__(self, name, value)

def __delattr__(self, name):
if not name.startswith("_"):
name = "_" + name
object.__delattr__(self, name)


a = A(1, 2, 3, b'value')
a.e = 4
print(a.e)
print(a.b)
try:
del a.c
print(a.c)
except AttributeError as ae:
print("AttributeError", ae)
2 changes: 2 additions & 0 deletions tests/run-tests
Expand Up @@ -228,6 +228,8 @@ def run_tests(pyb, tests, args):

if not has_coverage:
skip_tests.add('cmdline/cmd_parsetree.py')
skip_tests.add('basics/delsetattr.py')
skip_tests.add('basics/object_delgetsetattr.py')

# Some tests shouldn't be run on a PC
if pyb is None:
Expand Down
2 changes: 2 additions & 0 deletions unix/mpconfigport_coverage.h
Expand Up @@ -36,3 +36,5 @@
#define MICROPY_FSUSERMOUNT (1)
#define MICROPY_VFS_FAT (1)
#define MICROPY_PY_FRAMEBUF (1)
#define MICROPY_PY_METHODS_DELATTRS_SETATTRS (1)
#define MICROPY_PY_OBJECT_METHODS_DELATTRS_SETATTRS (1)