[pygobject] Add callable and descriptor protocols to PyGICallableInfo



commit 35f79b22ec5abf02fd0bb66352eb1f251b65a078
Author: Simon Feltman <sfeltman src gnome org>
Date:   Tue Jul 16 16:00:14 2013 -0700

    Add callable and descriptor protocols to PyGICallableInfo
    
    Add tp_call (__call__) function to callable info objects.
    This allows for replacement of wrapped invoke methods directly
    with the already created callable info object. This has the
    additional side effect of making doc strings lazily bound
    (only generated when __doc__ is accessed).
    
    Add tp_desc_get (__get__) to PyGIFunctionInfo which returns
    a bound version of itself for methods and constructors.
    
    Update various internal type checks to reflect the changes.
    Update tests to reflect the new callable type being the same
    across Python 2 & 3.
    
    This patch gives roughly a %17 speedup for Gtk imports and
    an %11 speedup for GI method calls.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=704037

 gi/module.py                |    5 +-
 gi/overrides/__init__.py    |   10 ++-
 gi/pygi-info.c              |  179 +++++++++++++++++++++++++++++++++++++++++--
 gi/pygi.h                   |   13 +++
 gi/types.py                 |   35 +--------
 tests/test_docstring.py     |    6 +-
 tests/test_gi.py            |   32 +++-----
 tests/test_overrides_gtk.py |    4 +-
 8 files changed, 212 insertions(+), 72 deletions(-)
---
diff --git a/gi/module.py b/gi/module.py
index 9b9dfbc..71f8ac5 100644
--- a/gi/module.py
+++ b/gi/module.py
@@ -57,8 +57,7 @@ from ._gi import \
     flags_register_new_gtype_and_add
 from .types import \
     GObjectMeta, \
-    StructMeta, \
-    Function
+    StructMeta
 
 from ._gobject._gobject import \
     GInterface, \
@@ -220,7 +219,7 @@ class IntrospectionModule(object):
                 g_type.pytype = wrapper
 
         elif isinstance(info, FunctionInfo):
-            wrapper = Function(info)
+            wrapper = info
         elif isinstance(info, ConstantInfo):
             wrapper = info.get_value()
         else:
diff --git a/gi/overrides/__init__.py b/gi/overrides/__init__.py
index 99cb152..943ea60 100644
--- a/gi/overrides/__init__.py
+++ b/gi/overrides/__init__.py
@@ -3,6 +3,7 @@ import warnings
 import functools
 
 from gi import PyGIDeprecationWarning
+from gi._gi import CallableInfo
 from gi._gobject.constants import \
     TYPE_NONE, \
     TYPE_INVALID
@@ -33,7 +34,8 @@ class _Registry(dict):
             raise TypeError('Can not override a type %s, which is not in a gobject introspection typelib' % 
value.__name__)
 
         if not value.__module__.startswith('gi.overrides'):
-            raise KeyError('You have tried to modify the registry outside of the overrides module.  This is 
not allowed')
+            raise KeyError('You have tried to modify the registry outside of the overrides module. '
+                           'This is not allowed (%s, %s)' % (value, value.__module__))
 
         g_type = info.get_g_type()
         assert g_type != TYPE_NONE
@@ -52,8 +54,8 @@ class _Registry(dict):
 class overridefunc(object):
     '''decorator for overriding a function'''
     def __init__(self, func):
-        if not hasattr(func, '__info__'):
-            raise TypeError("func must be an gi function")
+        if not isinstance(func, CallableInfo):
+            raise TypeError("func must be a gi function, got %s" % func)
         from ..importer import modules
         module_name = func.__module__.rsplit('.', 1)[-1]
         self.module = modules[module_name]._introspection_module
@@ -70,7 +72,7 @@ registry = _Registry()
 
 def override(type_):
     '''Decorator for registering an override'''
-    if isinstance(type_, types.FunctionType):
+    if isinstance(type_, (types.FunctionType, CallableInfo)):
         return overridefunc(type_)
     else:
         registry.register(type_)
diff --git a/gi/pygi-info.c b/gi/pygi-info.c
index 0622fc8..23ace9a 100644
--- a/gi/pygi-info.c
+++ b/gi/pygi-info.c
@@ -357,7 +357,7 @@ out:
 
 
 /* CallableInfo */
-PYGLIB_DEFINE_TYPE ("gi.CallableInfo", PyGICallableInfo_Type, PyGIBaseInfo);
+PYGLIB_DEFINE_TYPE ("gi.CallableInfo", PyGICallableInfo_Type, PyGICallableInfo);
 
 static PyObject *
 _wrap_g_callable_info_get_arguments (PyGIBaseInfo *self)
@@ -395,6 +395,164 @@ _wrap_g_callable_info_get_arguments (PyGIBaseInfo *self)
     return infos;
 }
 
+
+/* _callable_info_call:
+ *
+ * Shared wrapper for invoke which can be bound (instance method or class constructor)
+ * or unbound (function or static method).
+ */
+static PyObject *
+_callable_info_call (PyGICallableInfo *self, PyObject *args, PyObject *kwargs)
+{
+    /* Insert the bound arg at the beginning of the invoke method args. */
+    if (self->py_bound_arg) {
+        int i;
+        PyObject *result;
+        Py_ssize_t argcount = PyTuple_Size (args);
+        PyObject *newargs = PyTuple_New (argcount + 1);
+        if (newargs == NULL)
+            return NULL;
+
+        Py_INCREF (self->py_bound_arg);
+        PyTuple_SET_ITEM (newargs, 0, self->py_bound_arg);
+
+        for (i = 0; i < argcount; i++) {
+            PyObject *v = PyTuple_GET_ITEM (args, i);
+            Py_XINCREF (v);
+            PyTuple_SET_ITEM (newargs, i+1, v);
+        }
+
+        /* Invoke with the original GI info struct this wrapper was based upon.
+         * This is necessary to maintain the same cache for all bound versions.
+         */
+        result = _wrap_g_callable_info_invoke ((PyGIBaseInfo *)self->py_unbound_info,
+                                               newargs, kwargs);
+        Py_DECREF (newargs);
+        return result;
+
+    } else {
+        /* We should never have an unbound info when calling when calling invoke
+         * at this point because the descriptor implementation on sub-classes
+         * should return "self" not a copy when there is no bound arg.
+         */
+        g_assert (self->py_unbound_info == NULL);
+        return _wrap_g_callable_info_invoke ((PyGIBaseInfo *)self, args, kwargs);
+    }
+}
+
+
+/* _function_info_call:
+ *
+ * Specialization of _callable_info_call for GIFunctionInfo which
+ * handles constructor error conditions.
+ */
+static PyObject *
+_function_info_call (PyGICallableInfo *self, PyObject *args, PyObject *kwargs)
+{
+    if (self->py_bound_arg) {
+        GIFunctionInfoFlags flags;
+
+        /* Ensure constructors are only called as class methods on the class
+         * implementing the constructor and not on sub-classes.
+         */
+        flags = g_function_info_get_flags ( (GIFunctionInfo*) self->base.info);
+        if (flags & GI_FUNCTION_IS_CONSTRUCTOR) {
+            PyObject *py_str_name;
+            const gchar *str_name;
+            GIBaseInfo *container_info = g_base_info_get_container (self->base.info);
+            g_assert (container_info != NULL);
+
+            py_str_name = PyObject_GetAttrString (self->py_bound_arg, "__name__");
+            if (py_str_name == NULL)
+                return NULL;
+
+            if (PyUnicode_Check (py_str_name) ) {
+                PyObject *tmp = PyUnicode_AsUTF8String (py_str_name);
+                Py_DECREF (py_str_name);
+                py_str_name = tmp;
+            }
+
+#if PY_VERSION_HEX < 0x03000000
+            str_name = PyString_AsString (py_str_name);
+#else
+            str_name = PyBytes_AsString (py_str_name);
+#endif
+
+            if (strcmp (str_name, g_base_info_get_name (container_info))) {
+                PyErr_Format (PyExc_TypeError,
+                              "%s constructor cannot be used to create instances of "
+                              "a subclass %s",
+                              g_base_info_get_name (container_info),
+                              str_name);
+                Py_DECREF (py_str_name);
+                return NULL;
+            }
+            Py_DECREF (py_str_name);
+        }
+    }
+
+    return _callable_info_call (self, args, kwargs);
+}
+
+/* _new_bound_callable_info
+ *
+ * Utility function for sub-classes to create a bound version of themself.
+ */
+static PyGICallableInfo *
+_new_bound_callable_info (PyGICallableInfo *self, PyObject *bound_arg)
+{
+    PyGICallableInfo *new_self;
+
+    /* Return self if this is already bound or there is nothing passed to bind.  */
+    if (self->py_bound_arg != NULL || bound_arg == NULL || bound_arg == Py_None) {
+        Py_INCREF ((PyObject *)self);
+        return self;
+    }
+
+    new_self = (PyGICallableInfo *)_pygi_info_new (self->base.info);
+    if (new_self == NULL)
+        return NULL;
+
+    Py_INCREF ((PyObject *)self);
+    new_self->py_unbound_info = (struct PyGICallableInfo *)self;
+
+    Py_INCREF (bound_arg);
+    new_self->py_bound_arg = bound_arg;
+
+    return new_self;
+}
+
+/* _function_info_descr_get
+ *
+ * Descriptor protocol implementation for functions, methods, and constructors.
+ */
+static PyObject *
+_function_info_descr_get (PyGICallableInfo *self, PyObject *obj, PyObject *type) {
+    GIFunctionInfoFlags flags;
+    PyObject *bound_arg = NULL;
+
+    flags = g_function_info_get_flags ( (GIFunctionInfo*) self->base.info);
+    if (flags & GI_FUNCTION_IS_CONSTRUCTOR) {
+        if (type == NULL)
+            bound_arg = (PyObject *)(Py_TYPE(obj));
+        else
+            bound_arg = type;
+    } else if (flags & GI_FUNCTION_IS_METHOD) {
+        bound_arg = obj;
+    }
+
+    return (PyObject *)_new_bound_callable_info (self, bound_arg);
+}
+
+static void
+_callable_info_dealloc (PyGICallableInfo *self)
+{
+    Py_CLEAR (self->py_unbound_info);
+    Py_CLEAR (self->py_bound_arg);
+
+    PyGIBaseInfo_Type.tp_dealloc ((PyObject *) self);
+}
+
 static PyMethodDef _PyGICallableInfo_methods[] = {
     { "invoke", (PyCFunction) _wrap_g_callable_info_invoke, METH_VARARGS | METH_KEYWORDS },
     { "get_arguments", (PyCFunction) _wrap_g_callable_info_get_arguments, METH_NOARGS },
@@ -543,7 +701,7 @@ static PyMethodDef _PyGITypeInfo_methods[] = {
 
 
 /* FunctionInfo */
-PYGLIB_DEFINE_TYPE ("gi.FunctionInfo", PyGIFunctionInfo_Type, PyGIBaseInfo);
+PYGLIB_DEFINE_TYPE ("gi.FunctionInfo", PyGIFunctionInfo_Type, PyGICallableInfo);
 
 static PyObject *
 _wrap_g_function_info_is_constructor (PyGIBaseInfo *self)
@@ -729,7 +887,6 @@ static PyMethodDef _PyGIFunctionInfo_methods[] = {
     { NULL, NULL, 0 }
 };
 
-
 /* RegisteredTypeInfo */
 PYGLIB_DEFINE_TYPE ("gi.RegisteredTypeInfo", PyGIRegisteredTypeInfo_Type, PyGIBaseInfo);
 
@@ -1667,7 +1824,7 @@ static PyMethodDef _PyGIUnresolvedInfo_methods[] = {
 };
 
 /* GIVFuncInfo */
-PYGLIB_DEFINE_TYPE ("gi.VFuncInfo", PyGIVFuncInfo_Type, PyGIBaseInfo);
+PYGLIB_DEFINE_TYPE ("gi.VFuncInfo", PyGIVFuncInfo_Type, PyGICallableInfo);
 
 static PyObject *
 _wrap_g_vfunc_info_get_invoker (PyGIBaseInfo *self)
@@ -1835,14 +1992,20 @@ _pygi_info_register_types (PyObject *m)
     if (PyModule_AddObject(m, "DIRECTION_INOUT", PyLong_FromLong(GI_DIRECTION_INOUT)))
         return;
 
-    _PyGI_REGISTER_TYPE (m, PyGIUnresolvedInfo_Type, UnresolvedInfo,
-                         PyGIBaseInfo_Type);
     _PyGI_REGISTER_TYPE (m, PyGICallableInfo_Type, CallableInfo,
                          PyGIBaseInfo_Type);
-    _PyGI_REGISTER_TYPE (m, PyGICallbackInfo_Type, CallbackInfo,
-                         PyGIBaseInfo_Type);
+    PyGICallableInfo_Type.tp_call = (ternaryfunc) _callable_info_call;
+    PyGICallableInfo_Type.tp_dealloc = (destructor) _callable_info_dealloc;
+
     _PyGI_REGISTER_TYPE (m, PyGIFunctionInfo_Type, FunctionInfo,
                          PyGICallableInfo_Type);
+    PyGIFunctionInfo_Type.tp_call = (ternaryfunc) _function_info_call;
+    PyGIFunctionInfo_Type.tp_descr_get = (descrgetfunc) _function_info_descr_get;
+
+    _PyGI_REGISTER_TYPE (m, PyGIUnresolvedInfo_Type, UnresolvedInfo,
+                         PyGIBaseInfo_Type);
+    _PyGI_REGISTER_TYPE (m, PyGICallbackInfo_Type, CallbackInfo,
+                         PyGIBaseInfo_Type);
     _PyGI_REGISTER_TYPE (m, PyGIRegisteredTypeInfo_Type, RegisteredTypeInfo,
                          PyGIBaseInfo_Type);
     _PyGI_REGISTER_TYPE (m, PyGIStructInfo_Type, StructInfo,
diff --git a/gi/pygi.h b/gi/pygi.h
index 10da504..3bf40bb 100644
--- a/gi/pygi.h
+++ b/gi/pygi.h
@@ -47,6 +47,19 @@ typedef struct {
 } PyGIBaseInfo;
 
 typedef struct {
+    PyGIBaseInfo base;
+
+    /* Reference the unbound version of this struct.
+     * We use this for the actual call to invoke because it manages the cache.
+     */
+    struct PyGICallableInfo *py_unbound_info;
+
+    /* Holds bound argument for instance, class, and vfunc methods. */
+    PyObject *py_bound_arg;
+
+} PyGICallableInfo;
+
+typedef struct {
     PyGPointer base;
     gboolean free_on_dealloc;
 } PyGIStruct;
diff --git a/gi/types.py b/gi/types.py
index 4e95ca2..abc52d3 100644
--- a/gi/types.py
+++ b/gi/types.py
@@ -57,15 +57,6 @@ def wraps_callable_info(info):
     return update_func
 
 
-def Function(info):
-    """Wraps GIFunctionInfo"""
-    @wraps_callable_info(info)
-    def function(*args, **kwargs):
-        return info.invoke(*args, **kwargs)
-
-    return function
-
-
 class NativeVFunc(object):
     """Wraps GINativeVFuncInfo"""
     def __init__(self, info):
@@ -78,30 +69,10 @@ class NativeVFunc(object):
         return native_vfunc
 
 
-def Constructor(info):
-    """Wraps GIFunctionInfo with get_constructor() == True"""
-    @wraps_callable_info(info)
-    def constructor(cls, *args, **kwargs):
-        cls_name = info.get_container().get_name()
-        if cls.__name__ != cls_name:
-            raise TypeError('%s constructor cannot be used to create instances of a subclass' % cls_name)
-        return info.invoke(cls, *args, **kwargs)
-    return constructor
-
-
 class MetaClassHelper(object):
     def _setup_methods(cls):
         for method_info in cls.__info__.get_methods():
-            if method_info.is_method():
-                function = Function(method_info)
-                method = function
-            elif method_info.is_constructor():
-                function = Constructor(method_info)
-                method = classmethod(function)
-            else:
-                function = Function(method_info)
-                method = staticmethod(function)
-            setattr(cls, function.__name__, method)
+            setattr(cls, method_info.__name__, method_info)
 
     def _setup_fields(cls):
         for field_info in cls.__info__.get_fields():
@@ -326,7 +297,7 @@ class StructMeta(type, MetaClassHelper):
 
         for method_info in cls.__info__.get_methods():
             if method_info.is_constructor() and \
-                    method_info.get_name() == 'new' and \
+                    method_info.__name__ == 'new' and \
                     not method_info.get_arguments():
-                cls.__new__ = staticmethod(Constructor(method_info))
+                cls.__new__ = staticmethod(method_info)
                 break
diff --git a/tests/test_docstring.py b/tests/test_docstring.py
index 853f39d..1628295 100644
--- a/tests/test_docstring.py
+++ b/tests/test_docstring.py
@@ -21,14 +21,14 @@ class Test(unittest.TestCase):
                          old_func)
 
     def test_split_args_multi_out(self):
-        in_args, out_args = gi.docstring.split_function_info_args(GIMarshallingTests.int_out_out.__info__)
+        in_args, out_args = gi.docstring.split_function_info_args(GIMarshallingTests.int_out_out)
         self.assertEqual(len(in_args), 0)
         self.assertEqual(len(out_args), 2)
         self.assertEqual(out_args[0].get_pytype_hint(), 'int')
         self.assertEqual(out_args[1].get_pytype_hint(), 'int')
 
     def test_split_args_inout(self):
-        in_args, out_args = 
gi.docstring.split_function_info_args(GIMarshallingTests.long_inout_max_min.__info__)
+        in_args, out_args = gi.docstring.split_function_info_args(GIMarshallingTests.long_inout_max_min)
         self.assertEqual(len(in_args), 1)
         self.assertEqual(len(out_args), 1)
         self.assertEqual(in_args[0].get_name(), out_args[0].get_name())
@@ -36,7 +36,7 @@ class Test(unittest.TestCase):
 
     def test_split_args_none(self):
         obj = GIMarshallingTests.Object(int=33)
-        in_args, out_args = gi.docstring.split_function_info_args(obj.none_inout.__info__)
+        in_args, out_args = gi.docstring.split_function_info_args(obj.none_inout)
         self.assertEqual(len(in_args), 1)
         self.assertEqual(len(out_args), 1)
 
diff --git a/tests/test_gi.py b/tests/test_gi.py
index 60c2333..808d870 100644
--- a/tests/test_gi.py
+++ b/tests/test_gi.py
@@ -2329,16 +2329,12 @@ class TestInterfaces(unittest.TestCase):
             Gio.FileEnumerator.next_file(obj, None)
             self.fail('call with wrong type argument unexpectedly succeeded')
         except TypeError as e:
-            if sys.version_info < (3, 0):
-                self.assertTrue('FileEnumerator' in str(e), e)
-                self.assertTrue('Object' in str(e), e)
-            else:
-                # should have argument name
-                self.assertTrue('self' in str(e), e)
-                # should have expected type
-                self.assertTrue('xpected Gio.FileEnumerator' in str(e), e)
-                # should have actual type
-                self.assertTrue('GIMarshallingTests.Object' in str(e), e)
+            # should have argument name
+            self.assertTrue('self' in str(e), e)
+            # should have expected type
+            self.assertTrue('xpected Gio.FileEnumerator' in str(e), e)
+            # should have actual type
+            self.assertTrue('GIMarshallingTests.Object' in str(e), e)
 
         # wrong type for first argument: GObject
         var = GLib.Variant('s', 'mystring')
@@ -2359,16 +2355,12 @@ class TestInterfaces(unittest.TestCase):
             Gio.SimpleAction.activate(obj, obj)
             self.fail('call with wrong type argument unexpectedly succeeded')
         except TypeError as e:
-            if sys.version_info < (3, 0):
-                self.assertTrue('SimpleAction' in str(e), e)
-                self.assertTrue('Object' in str(e), e)
-            else:
-                # should have argument name
-                self.assertTrue('self' in str(e), e)
-                # should have expected type
-                self.assertTrue('xpected Gio.Action' in str(e), e)
-                # should have actual type
-                self.assertTrue('GIMarshallingTests.Object' in str(e), e)
+            # should have argument name
+            self.assertTrue('self' in str(e), e)
+            # should have expected type
+            self.assertTrue('xpected Gio.Action' in str(e), e)
+            # should have actual type
+            self.assertTrue('GIMarshallingTests.Object' in str(e), e)
 
 
 class TestMRO(unittest.TestCase):
diff --git a/tests/test_overrides_gtk.py b/tests/test_overrides_gtk.py
index 0d28540..f9d21c2 100644
--- a/tests/test_overrides_gtk.py
+++ b/tests/test_overrides_gtk.py
@@ -484,8 +484,8 @@ class TestGtk(unittest.TestCase):
 
         # these methods cannot be called because they require a valid drag on
         # a real GdkWindow. So we only check that they exist and are callable.
-        self.assertTrue(hasattr(widget.drag_dest_set_proxy, '__call__'))
-        self.assertTrue(hasattr(widget.drag_get_data, '__call__'))
+        self.assertTrue(hasattr(widget, 'drag_dest_set_proxy'))
+        self.assertTrue(hasattr(widget, 'drag_get_data'))
 
     def test_drag_target_list(self):
         mixed_target_list = [Gtk.TargetEntry.new('test0', 0, 0),


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]