From e44d26ae0c1b5d248fa4db112cdeabe404944f3c Mon Sep 17 00:00:00 2001
From: Damien George <damien.p.george@gmail.com>
Date: Mon, 31 Mar 2014 22:57:56 +0100
Subject: [PATCH] py: Implement __getattr__.

It's not completely satisfactory, because a failed call to __getattr__
should not raise an exception.

__setattr__ could be implemented, but it would slow down all stores to a
user created object.  Need to implement some caching system.
---
 py/objtype.c            | 14 ++++++++
 py/qstrdefs.h           |  1 +
 py/runtime.c            | 79 ++++++++++++++++++++---------------------
 py/runtime.h            |  1 +
 tests/basics/getattr.py | 11 ++++++
 5 files changed, 65 insertions(+), 41 deletions(-)
 create mode 100644 tests/basics/getattr.py

diff --git a/py/objtype.c b/py/objtype.c
index 06bc803eb..11200c913 100644
--- a/py/objtype.c
+++ b/py/objtype.c
@@ -16,6 +16,7 @@
 typedef struct _mp_obj_class_t {
     mp_obj_base_t base;
     mp_map_t members;
+    // TODO maybe cache __getattr__ and __setattr__ for efficient lookup of them
 } mp_obj_class_t;
 
 STATIC mp_obj_t mp_obj_new_class(mp_obj_t class) {
@@ -225,6 +226,19 @@ STATIC void class_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
         } else {
             // class member is a value, so just return that value
             dest[0] = member;
+        }
+        return;
+    }
+
+    // try __getattr__
+    if (attr != MP_QSTR___getattr__) {
+        mp_obj_t dest2[3];
+        mp_load_method_maybe(self_in, MP_QSTR___getattr__, dest2);
+        if (dest2[0] != MP_OBJ_NULL) {
+            // __getattr__ exists, call it and return its result
+            // XXX if this fails to load the requested attr, should we catch the attribute error and return silently?
+            dest2[2] = MP_OBJ_NEW_QSTR(attr);
+            dest[0] = mp_call_method_n_kw(1, 0, dest2);
             return;
         }
     }
diff --git a/py/qstrdefs.h b/py/qstrdefs.h
index 457043938..5c29ba1de 100644
--- a/py/qstrdefs.h
+++ b/py/qstrdefs.h
@@ -26,6 +26,7 @@ Q(__add__)
 Q(__sub__)
 Q(__repr__)
 Q(__str__)
+Q(__getattr__)
 
 Q(micropython)
 Q(byte_code)
diff --git a/py/runtime.c b/py/runtime.c
index f7a55545a..0c75d4cd3 100644
--- a/py/runtime.c
+++ b/py/runtime.c
@@ -701,7 +701,7 @@ mp_obj_t mp_load_attr(mp_obj_t base, qstr attr) {
 // no attribute found, returns:     dest[0] == MP_OBJ_NULL, dest[1] == MP_OBJ_NULL
 // normal attribute found, returns: dest[0] == <attribute>, dest[1] == MP_OBJ_NULL
 // method attribute found, returns: dest[0] == <method>,    dest[1] == <self>
-STATIC void mp_load_method_maybe(mp_obj_t base, qstr attr, mp_obj_t *dest) {
+void mp_load_method_maybe(mp_obj_t base, qstr attr, mp_obj_t *dest) {
     // clear output to indicate no attribute/method found yet
     dest[0] = MP_OBJ_NULL;
     dest[1] = MP_OBJ_NULL;
@@ -709,48 +709,45 @@ STATIC void mp_load_method_maybe(mp_obj_t base, qstr attr, mp_obj_t *dest) {
     // get the type
     mp_obj_type_t *type = mp_obj_get_type(base);
 
-    // if this type can do its own load, then call it
-    if (type->load_attr != NULL) {
-        type->load_attr(base, attr, dest);
-    }
-
-    // if nothing found yet, look for built-in and generic names
-    if (dest[0] == MP_OBJ_NULL) {
+    // look for built-in names
+    if (0) {
 #if MICROPY_CPYTHON_COMPAT
-        if (attr == MP_QSTR___class__) {
-            // a.__class__ is equivalent to type(a)
-            dest[0] = type;
-        } else
+    } else if (attr == MP_QSTR___class__) {
+        // a.__class__ is equivalent to type(a)
+        dest[0] = type;
 #endif
-        if (attr == MP_QSTR___next__ && type->iternext != NULL) {
-            dest[0] = (mp_obj_t)&mp_builtin_next_obj;
-            dest[1] = base;
-        } else if (type->load_attr == NULL) {
-            // generic method lookup if type didn't provide a specific one
-            // this is a lookup in the object (ie not class or type)
-            if (type->locals_dict != NULL) {
-                assert(MP_OBJ_IS_TYPE(type->locals_dict, &mp_type_dict)); // Micro Python restriction, for now
-                mp_map_t *locals_map = mp_obj_dict_get_map(type->locals_dict);
-                mp_map_elem_t *elem = mp_map_lookup(locals_map, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP);
-                if (elem != NULL) {
-                    // check if the methods are functions, static or class methods
-                    // see http://docs.python.org/3.3/howto/descriptor.html
-                    if (MP_OBJ_IS_TYPE(elem->value, &mp_type_staticmethod)) {
-                        // return just the function
-                        dest[0] = ((mp_obj_static_class_method_t*)elem->value)->fun;
-                    } else if (MP_OBJ_IS_TYPE(elem->value, &mp_type_classmethod)) {
-                        // return a bound method, with self being the type of this object
-                        dest[0] = ((mp_obj_static_class_method_t*)elem->value)->fun;
-                        dest[1] = mp_obj_get_type(base);
-                    } else if (mp_obj_is_callable(elem->value)) {
-                        // return a bound method, with self being this object
-                        dest[0] = elem->value;
-                        dest[1] = base;
-                    } else {
-                        // class member is a value, so just return that value
-                        dest[0] = elem->value;
-                    }
-                }
+
+    } else if (attr == MP_QSTR___next__ && type->iternext != NULL) {
+        dest[0] = (mp_obj_t)&mp_builtin_next_obj;
+        dest[1] = base;
+
+    } else if (type->load_attr != NULL) {
+        // this type can do its own load, so call it
+        type->load_attr(base, attr, dest);
+
+    } else if (type->locals_dict != NULL) {
+        // generic method lookup
+        // this is a lookup in the object (ie not class or type)
+        assert(MP_OBJ_IS_TYPE(type->locals_dict, &mp_type_dict)); // Micro Python restriction, for now
+        mp_map_t *locals_map = mp_obj_dict_get_map(type->locals_dict);
+        mp_map_elem_t *elem = mp_map_lookup(locals_map, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP);
+        if (elem != NULL) {
+            // check if the methods are functions, static or class methods
+            // see http://docs.python.org/3.3/howto/descriptor.html
+            if (MP_OBJ_IS_TYPE(elem->value, &mp_type_staticmethod)) {
+                // return just the function
+                dest[0] = ((mp_obj_static_class_method_t*)elem->value)->fun;
+            } else if (MP_OBJ_IS_TYPE(elem->value, &mp_type_classmethod)) {
+                // return a bound method, with self being the type of this object
+                dest[0] = ((mp_obj_static_class_method_t*)elem->value)->fun;
+                dest[1] = mp_obj_get_type(base);
+            } else if (mp_obj_is_callable(elem->value)) {
+                // return a bound method, with self being this object
+                dest[0] = elem->value;
+                dest[1] = base;
+            } else {
+                // class member is a value, so just return that value
+                dest[0] = elem->value;
             }
         }
     }
diff --git a/py/runtime.h b/py/runtime.h
index b817d61ae..b233d23b4 100644
--- a/py/runtime.h
+++ b/py/runtime.h
@@ -46,6 +46,7 @@ void mp_unpack_sequence(mp_obj_t seq, uint num, mp_obj_t *items);
 mp_obj_t mp_store_map(mp_obj_t map, mp_obj_t key, mp_obj_t value);
 mp_obj_t mp_load_attr(mp_obj_t base, qstr attr);
 void mp_load_method(mp_obj_t base, qstr attr, mp_obj_t *dest);
+void mp_load_method_maybe(mp_obj_t base, qstr attr, mp_obj_t *dest);
 void mp_store_attr(mp_obj_t base, qstr attr, mp_obj_t val);
 void mp_store_subscr(mp_obj_t base, mp_obj_t index, mp_obj_t val);
 
diff --git a/tests/basics/getattr.py b/tests/basics/getattr.py
new file mode 100644
index 000000000..a021e38fb
--- /dev/null
+++ b/tests/basics/getattr.py
@@ -0,0 +1,11 @@
+# test __getattr__
+
+class A:
+    def __init__(self, d):
+        self.d = d
+
+    def __getattr__(self, attr):
+        return self.d[attr]
+
+a = A({'a':1, 'b':2})
+print(a.a, a.b)
-- 
GitLab