Skip to content

Commit 18e6569

Browse files
dmazzelladpgeorge
authored andcommitted
py/objtype: Implement __delattr__ and __setattr__.
This patch implements support for class methods __delattr__ and __setattr__ for customising attribute access. It is controlled by the config option MICROPY_PY_DELATTR_SETATTR and is disabled by default.
1 parent ec7dc7f commit 18e6569

4 files changed

Lines changed: 104 additions & 0 deletions

File tree

py/mpconfig.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,12 @@ typedef double mp_float_t;
635635
#define MICROPY_PY_DESCRIPTORS (0)
636636
#endif
637637

638+
// Whether to support class __delattr__ and __setattr__ methods
639+
// This costs some code size and makes all del attrs and store attrs slow
640+
#ifndef MICROPY_PY_DELATTR_SETATTR
641+
#define MICROPY_PY_DELATTR_SETATTR (0)
642+
#endif
643+
638644
// Support for async/await/async for/async with
639645
#ifndef MICROPY_PY_ASYNC_AWAIT
640646
#define MICROPY_PY_ASYNC_AWAIT (1)

py/objtype.c

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,15 @@ STATIC void mp_obj_instance_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *des
533533

534534
// try __getattr__
535535
if (attr != MP_QSTR___getattr__) {
536+
#if MICROPY_PY_DELATTR_SETATTR
537+
// If the requested attr is __setattr__/__delattr__ then don't delegate the lookup
538+
// to __getattr__. If we followed CPython's behaviour then __setattr__/__delattr__
539+
// would have already been found in the "object" base class.
540+
if (attr == MP_QSTR___setattr__ || attr == MP_QSTR___delattr__) {
541+
return;
542+
}
543+
#endif
544+
536545
mp_obj_t dest2[3];
537546
mp_load_method_maybe(self_in, MP_QSTR___getattr__, dest2);
538547
if (dest2[0] != MP_OBJ_NULL) {
@@ -626,10 +635,35 @@ STATIC bool mp_obj_instance_store_attr(mp_obj_t self_in, qstr attr, mp_obj_t val
626635

627636
if (value == MP_OBJ_NULL) {
628637
// delete attribute
638+
#if MICROPY_PY_DELATTR_SETATTR
639+
// try __delattr__ first
640+
mp_obj_t attr_delattr_method[3];
641+
mp_load_method_maybe(self_in, MP_QSTR___delattr__, attr_delattr_method);
642+
if (attr_delattr_method[0] != MP_OBJ_NULL) {
643+
// __delattr__ exists, so call it
644+
attr_delattr_method[2] = MP_OBJ_NEW_QSTR(attr);
645+
mp_call_method_n_kw(1, 0, attr_delattr_method);
646+
return true;
647+
}
648+
#endif
649+
629650
mp_map_elem_t *elem = mp_map_lookup(&self->members, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP_REMOVE_IF_FOUND);
630651
return elem != NULL;
631652
} else {
632653
// store attribute
654+
#if MICROPY_PY_DELATTR_SETATTR
655+
// try __setattr__ first
656+
mp_obj_t attr_setattr_method[4];
657+
mp_load_method_maybe(self_in, MP_QSTR___setattr__, attr_setattr_method);
658+
if (attr_setattr_method[0] != MP_OBJ_NULL) {
659+
// __setattr__ exists, so call it
660+
attr_setattr_method[2] = MP_OBJ_NEW_QSTR(attr);
661+
attr_setattr_method[3] = value;
662+
mp_call_method_n_kw(2, 0, attr_setattr_method);
663+
return true;
664+
}
665+
#endif
666+
633667
mp_map_lookup(&self->members, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP_ADD_IF_NOT_FOUND)->value = value;
634668
return true;
635669
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# test __delattr__ and __setattr__
2+
3+
# feature test for __setattr__/__delattr__
4+
try:
5+
class Test():
6+
def __delattr__(self, attr): pass
7+
del Test().noexist
8+
except AttributeError:
9+
import sys
10+
print('SKIP')
11+
sys.exit()
12+
13+
# this class just prints the calls to see if they were executed
14+
class A():
15+
def __getattr__(self, attr):
16+
print('get', attr)
17+
return 1
18+
def __setattr__(self, attr, val):
19+
print('set', attr, val)
20+
def __delattr__(self, attr):
21+
print('del', attr)
22+
a = A()
23+
24+
# check basic behaviour
25+
print(getattr(a, 'foo'))
26+
setattr(a, 'bar', 2)
27+
delattr(a, 'baz')
28+
29+
# check meta behaviour
30+
getattr(a, '__getattr__') # should not call A.__getattr__
31+
getattr(a, '__setattr__') # should not call A.__getattr__
32+
getattr(a, '__delattr__') # should not call A.__getattr__
33+
setattr(a, '__setattr__', 1) # should call A.__setattr__
34+
delattr(a, '__delattr__') # should call A.__delattr__
35+
36+
# this class acts like a dictionary
37+
class B:
38+
def __init__(self, d):
39+
# store the dict in the class, not instance, so
40+
# we don't get infinite recursion in __getattr_
41+
B.d = d
42+
43+
def __getattr__(self, attr):
44+
if attr in B.d:
45+
return B.d[attr]
46+
else:
47+
raise AttributeError(attr)
48+
49+
def __setattr__(self, attr, value):
50+
B.d[attr] = value
51+
52+
def __delattr__(self, attr):
53+
del B.d[attr]
54+
55+
a = B({"a":1, "b":2})
56+
print(a.a, a.b)
57+
a.a = 3
58+
print(a.a, a.b)
59+
del a.a
60+
try:
61+
print(a.a)
62+
except AttributeError:
63+
print("AttributeError")

unix/mpconfigport_coverage.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
#include <mpconfigport.h>
3434

35+
#define MICROPY_PY_DELATTR_SETATTR (1)
3536
#define MICROPY_PY_BUILTINS_HELP (1)
3637
#define MICROPY_PY_BUILTINS_HELP_MODULES (1)
3738
#define MICROPY_PY_URANDOM_EXTRA_FUNCS (1)

0 commit comments

Comments
 (0)