[pygobject] Add support for default arguments annotated with allow-none



commit 510789d52e9e2fd863d26613f3282364eb175601
Author: Simon Feltman <sfeltman src gnome org>
Date:   Sun Jul 28 14:44:51 2013 -0700

    Add support for default arguments annotated with allow-none
    
    Support default value of NULL for tail end arguments which are
    marked with allow-none.
    The implementation uses a place holder object for un-supplied arguments
    which are annotated with allow-none. This is then used later during
    marshaling to supply NULL as the default.
    Additionally support an implicit default for callback user_data
    using the same technique.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=640812

 gi/gimodule.c             |    6 +++
 gi/pygi-cache.c           |  103 +++++++++++++++++++++++++++++----------------
 gi/pygi-cache.h           |    8 ++++
 gi/pygi-invoke.c          |   42 +++++++++++++------
 gi/pygi-marshal-from-py.c |    5 ++
 gi/pygi.h                 |    1 +
 tests/test_everything.py  |   16 +++++++
 tests/test_gi.py          |   30 +++++++++++++
 8 files changed, 162 insertions(+), 49 deletions(-)
---
diff --git a/gi/gimodule.c b/gi/gimodule.c
index 12dfb36..9dfbd4f 100644
--- a/gi/gimodule.c
+++ b/gi/gimodule.c
@@ -29,6 +29,7 @@
 #include <pyglib-python-compat.h>
 
 PyObject *PyGIDeprecationWarning;
+PyObject *_PyGIDefaultArgPlaceholder;
 
 static PyObject *
 _wrap_pyg_enum_add (PyObject *self,
@@ -656,6 +657,11 @@ PYGLIB_MODULE_START(_gi, "_gi")
                                                 PyExc_DeprecationWarning, NULL);
 #endif
 
+    /* Place holder object used to fill in "from Python" argument lists
+     * for values not supplied by the caller but support a GI default.
+     */
+    _PyGIDefaultArgPlaceholder = PyObject_New(PyObject, &PyType_Type);
+
     Py_INCREF(PyGIDeprecationWarning);
     PyModule_AddObject(module, "PyGIDeprecationWarning", PyGIDeprecationWarning);
 
diff --git a/gi/pygi-cache.c b/gi/pygi-cache.c
index f8573fc..8459449 100644
--- a/gi/pygi-cache.c
+++ b/gi/pygi-cache.c
@@ -523,6 +523,7 @@ _arg_cache_from_py_interface_callback_setup (PyGIArgCache *arg_cache,
         PyGIArgCache *user_data_arg_cache = _arg_cache_alloc ();
         user_data_arg_cache->meta_type = PYGI_META_ARG_TYPE_CHILD_WITH_PYARG;
         user_data_arg_cache->direction = PYGI_DIRECTION_FROM_PYTHON;
+        user_data_arg_cache->has_default = TRUE; /* always allow user data with a NULL default. */
         _pygi_callable_cache_set_arg (callable_cache, callback_cache->user_data_index,
                                       user_data_arg_cache);
     }
@@ -692,6 +693,26 @@ _arg_cache_new_for_interface (GIInterfaceInfo *iface_info,
     return arg_cache;
 }
 
+/* _arg_info_default_value
+ * info:
+ * arg: (out): GIArgument to fill in with default value.
+ *
+ * This is currently a place holder API which only supports "allow-none" pointer args.
+ * Once defaults are part of the GI API, we can replace this with: g_arg_info_default_value
+ * https://bugzilla.gnome.org/show_bug.cgi?id=558620
+ *
+ * Returns: TRUE if the given argument supports a default value and was filled in.
+ */
+static gboolean
+_arg_info_default_value (GIArgInfo *info, GIArgument *arg)
+{
+    if (g_arg_info_may_be_null (info)) {
+        arg->v_pointer = NULL;
+        return TRUE;
+    }
+    return FALSE;
+}
+
 PyGIArgCache *
 _arg_cache_new (GITypeInfo *type_info,
                 PyGICallableCache *callable_cache,
@@ -895,6 +916,11 @@ _arg_cache_new (GITypeInfo *type_info,
         arg_cache->type_info = type_info;
 
         if (arg_info != NULL) {
+            if (!arg_cache->has_default) {
+                /* It is possible has_default was set somewhere else */
+                arg_cache->has_default = _arg_info_default_value (arg_info,
+                                                                  &arg_cache->default_value);
+            }
             arg_cache->arg_name = g_base_info_get_name ((GIBaseInfo *) arg_info);
             arg_cache->allow_none = g_arg_info_may_be_null (arg_info);
 
@@ -908,40 +934,6 @@ _arg_cache_new (GITypeInfo *type_info,
     return arg_cache;
 }
 
-static void
-_arg_name_list_generate (PyGICallableCache *callable_cache)
-{
-    GSList * arg_name_list = NULL;
-    int i;
-
-    if (callable_cache->arg_name_hash == NULL) {
-        callable_cache->arg_name_hash = g_hash_table_new (g_str_hash, g_str_equal);
-    } else {
-        g_hash_table_remove_all (callable_cache->arg_name_hash);
-    }
-
-    for (i=0; i < _pygi_callable_cache_args_len (callable_cache); i++) {
-        PyGIArgCache *arg_cache = NULL;
-
-        arg_cache = _pygi_callable_cache_get_arg (callable_cache, i);
-
-        if (arg_cache->meta_type != PYGI_META_ARG_TYPE_CHILD &&
-            arg_cache->meta_type != PYGI_META_ARG_TYPE_CLOSURE &&
-                (arg_cache->direction == PYGI_DIRECTION_FROM_PYTHON ||
-                 arg_cache->direction == PYGI_DIRECTION_BIDIRECTIONAL)) {
-
-            gpointer arg_name = (gpointer)arg_cache->arg_name;
-
-            arg_name_list = g_slist_prepend (arg_name_list, arg_name);
-            if (arg_name != NULL) {
-                g_hash_table_insert (callable_cache->arg_name_hash, arg_name, arg_name);
-            }
-        }
-    }
-
-    callable_cache->arg_name_list = g_slist_reverse (arg_name_list);
-}
-
 static PyGIDirection
 _pygi_get_direction (PyGICallableCache *callable_cache, GIDirection gi_direction)
 {
@@ -1041,7 +1033,6 @@ _args_cache_generate (GICallableInfo *callable_info,
             _pygi_callable_cache_set_arg (callable_cache, arg_index, arg_cache);
 
             direction = PYGI_DIRECTION_FROM_PYTHON;
-            arg_cache->arg_name = g_base_info_get_name ((GIBaseInfo *) arg_info);
             arg_cache->direction = direction;
             arg_cache->meta_type = PYGI_META_ARG_TYPE_CLOSURE;
             arg_cache->c_arg_index = i;
@@ -1117,11 +1108,51 @@ _args_cache_generate (GICallableInfo *callable_info,
             g_base_info_unref (type_info);
         }
 
+        /* Ensure arguments always have a name when available */
+        arg_cache->arg_name = g_base_info_get_name ((GIBaseInfo *) arg_info);
+
         g_base_info_unref ( (GIBaseInfo *)arg_info);
 
     }
 
-    _arg_name_list_generate (callable_cache);
+    if (callable_cache->arg_name_hash == NULL) {
+        callable_cache->arg_name_hash = g_hash_table_new (g_str_hash, g_str_equal);
+    } else {
+        g_hash_table_remove_all (callable_cache->arg_name_hash);
+    }
+    callable_cache->n_py_required_args = 0;
+
+    /* Reverse loop through all the arguments to setup arg_name_list/hash
+     * and find the number of required arguments */
+    for (i=((gssize)_pygi_callable_cache_args_len (callable_cache))-1; i >= 0; i--) {
+        PyGIArgCache *arg_cache = _pygi_callable_cache_get_arg (callable_cache, i);
+
+        if (arg_cache->meta_type != PYGI_META_ARG_TYPE_CHILD &&
+                arg_cache->meta_type != PYGI_META_ARG_TYPE_CLOSURE &&
+                arg_cache->direction & PYGI_DIRECTION_FROM_PYTHON) {
+
+            /* Setup arg_name_list and arg_name_hash */
+            gpointer arg_name = (gpointer)arg_cache->arg_name;
+            callable_cache->arg_name_list = g_slist_prepend (callable_cache->arg_name_list,
+                                                             arg_name);
+            if (arg_name != NULL) {
+                g_hash_table_insert (callable_cache->arg_name_hash,
+                                     arg_name,
+                                     GINT_TO_POINTER(i));
+            }
+
+            /* The first tail argument without a default will force all the preceding
+             * argument defaults off. This limits support of default args to the
+             * tail of an args list.
+             */
+            if (callable_cache->n_py_required_args > 0) {
+                arg_cache->has_default = FALSE;
+                callable_cache->n_py_required_args += 1;
+            } else if (!arg_cache->has_default) {
+                callable_cache->n_py_required_args += 1;
+            }
+        }
+    }
 
     return TRUE;
 }
diff --git a/gi/pygi-cache.h b/gi/pygi-cache.h
index e0ef5d3..65e2de5 100644
--- a/gi/pygi-cache.h
+++ b/gi/pygi-cache.h
@@ -105,6 +105,7 @@ struct _PyGIArgCache
     gboolean is_caller_allocates;
     gboolean is_skipped;
     gboolean allow_none;
+    gboolean has_default;
 
     PyGIDirection direction;
     GITransfer transfer;
@@ -121,6 +122,9 @@ struct _PyGIArgCache
 
     gssize c_arg_index;
     gssize py_arg_index;
+
+    /* Set when has_default is true. */
+    GIArgument default_value;
 };
 
 typedef struct _PyGISequenceCache
@@ -188,6 +192,10 @@ struct _PyGICallableCache
 
     /* Number of Python arguments expected for invoking the gi function. */
     gssize n_py_args;
+
+    /* Minimum number of args required to call the callable from Python.
+     * This count does not include args with defaults. */
+    gssize n_py_required_args;
 };
 
 void _pygi_arg_cache_clear     (PyGIArgCache *cache);
diff --git a/gi/pygi-invoke.c b/gi/pygi-invoke.c
index 7abd9e6..caa72be 100644
--- a/gi/pygi-invoke.c
+++ b/gi/pygi-invoke.c
@@ -120,7 +120,11 @@ _check_for_unexpected_kwargs (const gchar *function_name,
             }
         }
 
-        if (g_hash_table_lookup (arg_name_hash, PyBytes_AsString(key)) == NULL) {
+        /* Use extended lookup because it returns whether or not the key actually
+         * exists in the hash table. g_hash_table_lookup returns NULL for keys not
+         * found which maps to index 0 for our hash lookup.
+         */
+        if (!g_hash_table_lookup_extended (arg_name_hash, PyBytes_AsString(key), NULL, NULL)) {
             PyErr_Format (PyExc_TypeError,
                           "%.200s() got an unexpected keyword argument '%.400s'",
                           function_name,
@@ -209,17 +213,27 @@ _py_args_combine_and_check_length (PyGICallableCache *cache,
             PyTuple_SET_ITEM (combined_py_args, i, kw_arg_item);
 
         } else if (kw_arg_item == NULL && py_arg_item == NULL) {
-            PyErr_Format (PyExc_TypeError,
-                          "%.200s() takes exactly %d %sargument%s (%zd given)",
-                          function_name,
-                          n_expected_args,
-                          n_py_kwargs > 0 ? "non-keyword " : "",
-                          n_expected_args == 1 ? "" : "s",
-                          n_py_args);
-
-            Py_DECREF (combined_py_args);
-            return NULL;
-
+            /* If the argument supports a default, use a place holder in the
+             * argument tuple, this will be checked later during marshaling.
+             */
+            int arg_cache_index = -1;
+            if (arg_name != NULL)
+                arg_cache_index = GPOINTER_TO_INT (g_hash_table_lookup (cache->arg_name_hash, arg_name));
+            if (arg_cache_index >= 0 && _pygi_callable_cache_get_arg (cache, arg_cache_index)->has_default) {
+                Py_INCREF (_PyGIDefaultArgPlaceholder);
+                PyTuple_SET_ITEM (combined_py_args, i, _PyGIDefaultArgPlaceholder);
+            } else {
+                PyErr_Format (PyExc_TypeError,
+                              "%.200s() takes exactly %d %sargument%s (%zd given)",
+                              function_name,
+                              n_expected_args,
+                              n_py_kwargs > 0 ? "non-keyword " : "",
+                              n_expected_args == 1 ? "" : "s",
+                              n_py_args);
+
+                Py_DECREF (combined_py_args);
+                return NULL;
+            }
         } else if (kw_arg_item != NULL && py_arg_item != NULL) {
             PyErr_Format (PyExc_TypeError,
                           "%.200s() got multiple values for keyword argument '%.200s'",
@@ -490,7 +504,9 @@ _invoke_marshal_in_args (PyGIInvokeState *state, PyGICallableCache *cache)
         }
 
         c_arg = state->args[i];
-        if (arg_cache->from_py_marshaller != NULL) {
+        if (py_arg == _PyGIDefaultArgPlaceholder) {
+            *c_arg = arg_cache->default_value;
+        } else if (arg_cache->from_py_marshaller != NULL) {
             gboolean success;
             if (!arg_cache->allow_none && py_arg == Py_None) {
                 PyErr_Format (PyExc_TypeError,
diff --git a/gi/pygi-marshal-from-py.c b/gi/pygi-marshal-from-py.c
index 73356b1..22571ef 100644
--- a/gi/pygi-marshal-from-py.c
+++ b/gi/pygi-marshal-from-py.c
@@ -1255,6 +1255,11 @@ _pygi_marshal_from_py_interface_callback (PyGIInvokeState   *state,
             py_user_data = PyTuple_GetItem (state->py_in_args, user_data_cache->py_arg_index);
             if (!py_user_data)
                 return FALSE;
+            /* NULL out user_data if it was not supplied and the default arg placeholder
+             * was used instead.
+             */
+            if (py_user_data == _PyGIDefaultArgPlaceholder)
+                py_user_data = NULL;
         }
     }
 
diff --git a/gi/pygi.h b/gi/pygi.h
index 3bf40bb..dcd91b3 100644
--- a/gi/pygi.h
+++ b/gi/pygi.h
@@ -33,6 +33,7 @@
 #include "pygi-cache.h"
 
 extern PyObject *PyGIDeprecationWarning;
+extern PyObject *_PyGIDefaultArgPlaceholder;
 
 typedef struct {
     PyObject_HEAD
diff --git a/tests/test_everything.py b/tests/test_everything.py
index b2f0528..0c6533a 100644
--- a/tests/test_everything.py
+++ b/tests/test_everything.py
@@ -717,6 +717,22 @@ class TestCallbacks(unittest.TestCase):
 
         self.assertEqual(TestCallbacks.called, 100)
 
+    def test_callback_userdata_none_default_arg(self):
+        TestCallbacks.called = 0
+        userdata_list = []
+
+        def callback(userdata):
+            userdata_list.append(userdata)
+            TestCallbacks.called += 1
+            return TestCallbacks.called
+
+        for i in range(100):
+            val = Everything.test_callback_user_data(callback)
+            self.assertEqual(val, i + 1)
+
+        self.assertEqual(TestCallbacks.called, 100)
+        self.assertSequenceEqual(userdata_list, [None] * 100)
+
     def test_async_ready_callback(self):
         TestCallbacks.called = False
         TestCallbacks.main_loop = GLib.MainLoop()
diff --git a/tests/test_gi.py b/tests/test_gi.py
index da55187..8cb1c12 100644
--- a/tests/test_gi.py
+++ b/tests/test_gi.py
@@ -2642,6 +2642,36 @@ class TestKeywordArgs(unittest.TestCase):
         GIMarshallingTests.int_three_in_three_out(1, c=4, **d)
         self.assertEqual(d, d2)
 
+    @unittest.skipUnless(hasattr(GIMarshallingTests, 'int_one_in_utf8_two_in_one_allows_none'),
+                         'Requires newer GIMarshallingTests')
+    def test_allow_none_as_default(self):
+        GIMarshallingTests.int_two_in_utf8_two_in_with_allow_none(1, 2, '3', '4')
+        GIMarshallingTests.int_two_in_utf8_two_in_with_allow_none(1, 2, '3')
+        GIMarshallingTests.int_two_in_utf8_two_in_with_allow_none(1, 2)
+        GIMarshallingTests.int_two_in_utf8_two_in_with_allow_none(1, 2, d='4')
+
+        GIMarshallingTests.array_in_utf8_two_in_out_of_order('1', [-1, 0, 1, 2])
+        GIMarshallingTests.array_in_utf8_two_in_out_of_order('1', [-1, 0, 1, 2], '2')
+        self.assertRaises(TypeError,
+                          GIMarshallingTests.array_in_utf8_two_in_out_of_order,
+                          [-1, 0, 1, 2], a='1')
+        self.assertRaises(TypeError,
+                          GIMarshallingTests.array_in_utf8_two_in_out_of_order,
+                          [-1, 0, 1, 2])
+
+        GIMarshallingTests.array_in_utf8_two_in([-1, 0, 1, 2], '1', '2')
+        GIMarshallingTests.array_in_utf8_two_in([-1, 0, 1, 2], '1')
+        GIMarshallingTests.array_in_utf8_two_in([-1, 0, 1, 2])
+        GIMarshallingTests.array_in_utf8_two_in([-1, 0, 1, 2], b='2')
+
+        GIMarshallingTests.int_one_in_utf8_two_in_one_allows_none(1, '2', '3')
+        self.assertRaises(TypeError,
+                          GIMarshallingTests.int_one_in_utf8_two_in_one_allows_none,
+                          1, '3')
+        self.assertRaises(TypeError,
+                          GIMarshallingTests.int_one_in_utf8_two_in_one_allows_none,
+                          1, c='3')
+
 
 class TestPropertiesObject(unittest.TestCase):
 


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