Commit 510789d5 authored by Simon Feltman's avatar Simon Feltman

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
parent 03f531ff
......@@ -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);
......
......@@ -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;
}
......
......@@ -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);
......
......@@ -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,
......
......@@ -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;
}
}
......
......@@ -33,6 +33,7 @@
#include "pygi-cache.h"
extern PyObject *PyGIDeprecationWarning;
extern PyObject *_PyGIDefaultArgPlaceholder;
typedef struct {
PyObject_HEAD
......
......@@ -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()
......
......@@ -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):
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment