[pygobject] Use a named tuple for returning multiple values



commit 175d10665472e6f4090d707e3b89255814c932b1
Author: Christoph Reiter <creiter src gnome org>
Date:   Mon Jun 8 18:14:08 2015 +0200

    Use a named tuple for returning multiple values
    
    >>> v = Gtk.Button().get_alignment()
    >>> v
    (xalign=0.5, yalign=0.5)
    >>> v.xalign
    0.5
    
    For each GICallable a new gi._gi.ResultTuple subclass
    is created which knows the return value names of that
    callable and displays them in __repr__, __dir__ and
    allows to access tuple items by name.
    
    The subclass is cached in PyGICallableCache.
    
    To reduce the number of small tuple allocations use a free list
    similar to the one used for pure tuples in CPython.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=727374

 gi/Makefile.am            |    2 +
 gi/gimodule.c             |    1 +
 gi/pygi-cache.c           |   34 +++++
 gi/pygi-cache.h           |    6 +
 gi/pygi-invoke.c          |   35 +++--
 gi/pygi-private.h         |    1 +
 gi/pygi-resulttuple.c     |  354 +++++++++++++++++++++++++++++++++++++++++++++
 gi/pygi-resulttuple.h     |   34 +++++
 gi/pyglib-python-compat.h |    5 +
 tests/Makefile.am         |    1 +
 tests/test_resulttuple.py |   87 +++++++++++
 11 files changed, 543 insertions(+), 17 deletions(-)
---
diff --git a/gi/Makefile.am b/gi/Makefile.am
index 2e61af8..314fd41 100644
--- a/gi/Makefile.am
+++ b/gi/Makefile.am
@@ -78,6 +78,8 @@ _gi_la_SOURCES = \
        pygi-source.h \
        pygi-argument.c \
        pygi-argument.h \
+       pygi-resulttuple.c \
+       pygi-resulttuple.h \
        pygi-type.c \
        pygi-type.h \
        pygi-boxed.c \
diff --git a/gi/gimodule.c b/gi/gimodule.c
index cc8fd66..44f0901 100644
--- a/gi/gimodule.c
+++ b/gi/gimodule.c
@@ -664,6 +664,7 @@ PYGLIB_MODULE_START(_gi, "_gi")
     _pygi_struct_register_types (module);
     _pygi_boxed_register_types (module);
     _pygi_ccallback_register_types (module);
+    pygi_resulttuple_register_types (module);
 
     PyGIWarning = PyErr_NewException ("gi.PyGIWarning", PyExc_Warning, NULL);
 
diff --git a/gi/pygi-cache.c b/gi/pygi-cache.c
index 84cc463..0869e39 100644
--- a/gi/pygi-cache.c
+++ b/gi/pygi-cache.c
@@ -465,6 +465,9 @@ _callable_cache_generate_args_cache_real (PyGICallableCache *callable_cache,
     PyGIArgCache *return_cache;
     PyGIDirection return_direction;
        gssize last_explicit_arg_index;
+    PyObject *tuple_names;
+    GSList *arg_cache_item;
+    PyTypeObject* resulttuple_type;
 
     /* Return arguments are always considered out */
     return_direction = _pygi_get_direction (callable_cache, GI_DIRECTION_OUT);
@@ -641,6 +644,36 @@ _callable_cache_generate_args_cache_real (PyGICallableCache *callable_cache,
         }
     }
 
+    if (!return_cache->is_skipped && return_cache->type_tag != GI_TYPE_TAG_VOID) {
+        callable_cache->has_return = TRUE;
+    }
+
+    tuple_names = PyList_New (0);
+    if (callable_cache->has_return) {
+        PyList_Append (tuple_names, Py_None);
+    }
+
+    arg_cache_item = callable_cache->to_py_args;
+    while (arg_cache_item) {
+        const gchar *arg_name = ((PyGIArgCache *)arg_cache_item->data)->arg_name;
+        PyObject *arg_string = PYGLIB_PyUnicode_FromString (arg_name);
+        PyList_Append (tuple_names, arg_string);
+        Py_DECREF (arg_string);
+        arg_cache_item = arg_cache_item->next;
+    }
+
+    /* No need to create a tuple type if there aren't multiple values */
+    if (PyList_Size (tuple_names) > 1) {
+        resulttuple_type = pygi_resulttuple_new_type (tuple_names);
+        if (resulttuple_type == NULL) {
+            Py_DECREF (tuple_names);
+            return FALSE;
+        } else {
+            callable_cache->resulttuple_type = resulttuple_type;
+        }
+    }
+    Py_DECREF (tuple_names);
+
     return TRUE;
 }
 
@@ -651,6 +684,7 @@ _callable_cache_deinit_real (PyGICallableCache *cache)
     g_slist_free (cache->arg_name_list);
     g_hash_table_destroy (cache->arg_name_hash);
     g_ptr_array_unref (cache->args_cache);
+    Py_XDECREF (cache->resulttuple_type);
 
     if (cache->return_cache != NULL)
         pygi_arg_cache_free (cache->return_cache);
diff --git a/gi/pygi-cache.h b/gi/pygi-cache.h
index ad41755..4dfabd8 100644
--- a/gi/pygi-cache.h
+++ b/gi/pygi-cache.h
@@ -183,6 +183,12 @@ struct _PyGICallableCache
      * This is used for the length of PyGIInvokeState.out_values */
     gssize n_to_py_args;
 
+    /* If the callable return value gets used */
+    gboolean has_return;
+
+    /* The type used for returning multiple values or NULL */
+    PyTypeObject* resulttuple_type;
+
     /* Number of out args for g_function_info_invoke that will be skipped
      * when marshaling to Python due to them being implicitly available
      * (list/array length).
diff --git a/gi/pygi-invoke.c b/gi/pygi-invoke.c
index 0290fc9..02de46e 100644
--- a/gi/pygi-invoke.c
+++ b/gi/pygi-invoke.c
@@ -553,8 +553,7 @@ _invoke_marshal_out_args (PyGIInvokeState *state, PyGIFunctionCache *function_ca
     PyGICallableCache *cache = (PyGICallableCache *) function_cache;
     PyObject *py_out = NULL;
     PyObject *py_return = NULL;
-    gssize total_out_args = cache->n_to_py_args;
-    gboolean has_return = FALSE;
+    gssize n_out_args = cache->n_to_py_args - cache->n_to_py_child_args;
 
     if (cache->return_cache) {
         if (!cache->return_cache->is_skipped) {
@@ -567,12 +566,6 @@ _invoke_marshal_out_args (PyGIInvokeState *state, PyGIFunctionCache *function_ca
                                                        cache);
                 return NULL;
             }
-
-
-            if (cache->return_cache->type_tag != GI_TYPE_TAG_VOID) {
-                total_out_args++;
-                has_return = TRUE;
-            }
         } else {
             if (cache->return_cache->transfer == GI_TRANSFER_EVERYTHING) {
                 PyGIMarshalCleanupFunc to_py_cleanup =
@@ -588,9 +581,7 @@ _invoke_marshal_out_args (PyGIInvokeState *state, PyGIFunctionCache *function_ca
         }
     }
 
-    total_out_args -= cache->n_to_py_child_args;
-
-    if (cache->n_to_py_args - cache->n_to_py_child_args  == 0) {
+    if (n_out_args == 0) {
         if (cache->return_cache->is_skipped && state->error == NULL) {
             /* we skip the return value and have no (out) arguments to return,
              * so py_return should be NULL. But we must not return NULL,
@@ -602,7 +593,7 @@ _invoke_marshal_out_args (PyGIInvokeState *state, PyGIFunctionCache *function_ca
         }
 
         py_out = py_return;
-    } else if (total_out_args == 1) {
+    } else if (!cache->has_return && n_out_args == 1) {
         /* if we get here there is one out arg an no return */
         PyGIArgCache *arg_cache = (PyGIArgCache *)cache->to_py_args->data;
         py_out = arg_cache->to_py_marshaller (state,
@@ -617,16 +608,26 @@ _invoke_marshal_out_args (PyGIInvokeState *state, PyGIFunctionCache *function_ca
         }
 
     } else {
+        /* return a tuple */
         gssize py_arg_index = 0;
         GSList *cache_item = cache->to_py_args;
-        /* return a tuple */
-        py_out = PyTuple_New (total_out_args);
-        if (has_return) {
+        gssize tuple_len = cache->has_return + n_out_args;
+
+        py_out = pygi_resulttuple_new (cache->resulttuple_type, tuple_len);
+
+        if (py_out == NULL) {
+            pygi_marshal_cleanup_args_to_py_parameter_fail (state,
+                                                            cache,
+                                                            py_arg_index);
+            return NULL;
+        }
+
+        if (cache->has_return) {
             PyTuple_SET_ITEM (py_out, py_arg_index, py_return);
             py_arg_index++;
         }
 
-        for(; py_arg_index < total_out_args; py_arg_index++) {
+        for (; py_arg_index < tuple_len; py_arg_index++) {
             PyGIArgCache *arg_cache = (PyGIArgCache *)cache_item->data;
             PyObject *py_obj = arg_cache->to_py_marshaller (state,
                                                             cache,
@@ -634,7 +635,7 @@ _invoke_marshal_out_args (PyGIInvokeState *state, PyGIFunctionCache *function_ca
                                                             
state->args[arg_cache->c_arg_index].arg_pointer.v_pointer);
 
             if (py_obj == NULL) {
-                if (has_return)
+                if (cache->has_return)
                     py_arg_index--;
 
                 pygi_marshal_cleanup_args_to_py_parameter_fail (state,
diff --git a/gi/pygi-private.h b/gi/pygi-private.h
index af12f1c..f70aec4 100644
--- a/gi/pygi-private.h
+++ b/gi/pygi-private.h
@@ -32,6 +32,7 @@
 #include "pygi-invoke.h"
 #include "pygi-cache.h"
 #include "pygi-source.h"
+#include "pygi-resulttuple.h"
 
 G_BEGIN_DECLS
 #if PY_VERSION_HEX >= 0x03000000
diff --git a/gi/pygi-resulttuple.c b/gi/pygi-resulttuple.c
new file mode 100644
index 0000000..9d0b455
--- /dev/null
+++ b/gi/pygi-resulttuple.c
@@ -0,0 +1,354 @@
+/* -*- Mode: C; c-basic-offset: 4 -*-
+ * vim: tabstop=4 shiftwidth=4 expandtab
+ *
+ * Copyright (C) 2015 Christoph Reiter <reiter christoph gmail com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "pygi-resulttuple.h"
+#include "pyglib-private.h"
+
+static char repr_format_key[] = "__repr_format";
+static char tuple_indices_key[] = "__tuple_indices";
+
+/* A free list similar to the one used for the CPython tuple. Difference
+ * is that zero length tuples aren't cached (as we don't need them)
+ * and that the freelist is smaller as we don't free it with the cyclic GC
+ * as CPython does. This wastes 21kB max.
+ */
+#define PyGIResultTuple_MAXSAVESIZE 10
+#define PyGIResultTuple_MAXFREELIST 100
+static PyObject *free_list[PyGIResultTuple_MAXSAVESIZE];
+static int numfree[PyGIResultTuple_MAXSAVESIZE];
+
+PYGLIB_DEFINE_TYPE ("gi._gi.ResultTuple", PyGIResultTuple_Type, PyTupleObject)
+
+/**
+ * ResultTuple.__repr__() implementation.
+ * Takes the _ResultTuple.__repr_format format string and applies the tuple
+ * values to it.
+ */
+static PyObject*
+resulttuple_repr(PyObject *self) {
+    PyObject *format,  *repr, *format_attr;
+
+    format_attr = PYGLIB_PyUnicode_FromString (repr_format_key);
+    format = PyTuple_Type.tp_getattro (self, format_attr);
+    Py_DECREF (format_attr);
+    if (format == NULL)
+        return NULL;
+    repr = PYGLIB_PyUnicode_Format (format, self);
+    Py_DECREF (format);
+    return repr;
+}
+
+/**
+ * PyGIResultTuple_Type.tp_getattro implementation.
+ * Looks up the tuple index in _ResultTuple.__tuple_indices and returns the
+ * tuple item.
+ */
+static PyObject*
+resulttuple_getattro(PyObject *self, PyObject *name) {
+    PyObject *mapping, *index, *mapping_attr, *item;
+
+    mapping_attr = PYGLIB_PyUnicode_FromString (tuple_indices_key);
+    mapping = PyTuple_Type.tp_getattro (self, mapping_attr);
+    Py_DECREF (mapping_attr);
+    if (mapping == NULL)
+        return NULL;
+    g_assert (PyDict_Check (mapping));
+    index = PyDict_GetItem (mapping, name);
+
+    if (index != NULL) {
+        item = PyTuple_GET_ITEM (self, PYGLIB_PyLong_AsSsize_t (index));
+        Py_INCREF (item);
+    } else {
+        item = PyTuple_Type.tp_getattro (self, name);
+    }
+    Py_DECREF (mapping);
+
+    return item;
+}
+
+/**
+ * ResultTuple.__reduce__() implementation.
+ * Always returns (tuple, tuple(self))
+ * Needed so that pickling doesn't depend on our tuple subclass and unpickling
+ * works without it. As a result unpickle will give back in a normal tuple.
+ */
+static PyObject *
+resulttuple_reduce(PyObject *self)
+{
+    PyObject *tuple = PySequence_Tuple (self);
+    if (tuple == NULL)
+        return NULL;
+    return Py_BuildValue ("(O, (N))", &PyTuple_Type, tuple);
+}
+
+/**
+ * Extends __dir__ with the extra attributes accessible through
+ * resulttuple_getattro()
+ */
+static PyObject *
+resulttuple_dir(PyObject *self)
+{
+    PyObject *mapping_attr;
+    PyObject *items = NULL;
+    PyObject *mapping = NULL;
+    PyObject *mapping_values = NULL;
+    PyObject *result = NULL;
+
+    mapping_attr = PYGLIB_PyUnicode_FromString (tuple_indices_key);
+    mapping = PyTuple_Type.tp_getattro (self, mapping_attr);
+    Py_DECREF (mapping_attr);
+    if (mapping == NULL)
+        goto error;
+    items = PyObject_Dir ((PyObject*)self->ob_type);
+    if (items == NULL)
+        goto error;
+    mapping_values = PyDict_Keys (mapping);
+    if (mapping_values == NULL)
+        goto error;
+    result = PySequence_InPlaceConcat (items, mapping_values);
+
+error:
+    Py_XDECREF (items);
+    Py_XDECREF (mapping);
+    Py_XDECREF (mapping_values);
+
+    return result;
+}
+
+/**
+ * resulttuple_new_type:
+ * @args: one list object containing tuple item names and None
+ *
+ * Exposes pygi_resulttuple_new_type() as ResultTuple._new_type()
+ * to allow creation of result types for unit tests.
+ *
+ * Returns: A new PyTypeObject which is a subclass of PyGIResultTuple_Type
+ *    or %NULL in case of an error.
+ */
+static PyObject *
+resulttuple_new_type(PyObject *self, PyObject *args) {
+    PyObject *tuple_names, *new_type;
+
+    if (!PyArg_ParseTuple (args, "O:ResultTuple._new_type", &tuple_names))
+        return NULL;
+
+    if (!PyList_Check (tuple_names)) {
+        Py_DECREF (tuple_names);
+        PyErr_SetString (PyExc_TypeError, "not a list");
+        return NULL;
+    }
+
+    new_type = (PyObject *)pygi_resulttuple_new_type (tuple_names);
+    Py_DECREF (tuple_names);
+    return new_type;
+}
+
+static PyMethodDef resulttuple_methods[] = {
+    {"__reduce__", (PyCFunction)resulttuple_reduce, METH_NOARGS},
+    {"__dir__", (PyCFunction)resulttuple_dir, METH_NOARGS},
+    {"_new_type", (PyCFunction)resulttuple_new_type,
+     METH_VARARGS | METH_STATIC},
+    {NULL, NULL, 0},
+};
+
+/**
+ * pygi_resulttuple_new_type:
+ * @tuple_names: A python list containing str or None items.
+ *
+ * Similar to namedtuple() creates a new tuple subclass which
+ * allows to access items by name and have a pretty __repr__.
+ * Each item in the passed name list corresponds to an item with
+ * the same index in the tuple class. If the name is None the item/index
+ * is unnamed.
+ *
+ * Returns: A new PyTypeObject which is a subclass of PyGIResultTuple_Type
+ *    or %NULL in case of an error.
+ */
+PyTypeObject*
+pygi_resulttuple_new_type(PyObject *tuple_names) {
+    PyTypeObject *new_type;
+    PyObject *class_dict, *format_string, *empty_format, *named_format,
+        *format_list, *sep, *index_dict, *slots, *paren_format, *new_type_args,
+        *paren_string;
+    Py_ssize_t len, i;
+
+    g_assert (PyList_Check (tuple_names));
+
+    class_dict = PyDict_New ();
+
+    /* To save some memory don't use an instance dict */
+    slots = PyTuple_New (0);
+    PyDict_SetItemString (class_dict, "__slots__", slots);
+    Py_DECREF (slots);
+
+    format_list = PyList_New (0);
+    index_dict = PyDict_New ();
+
+    empty_format = PYGLIB_PyUnicode_FromString ("%r");
+    named_format = PYGLIB_PyUnicode_FromString ("%s=%%r");
+    len = PyList_Size (tuple_names);
+    for (i = 0; i < len; i++) {
+        PyObject *item, *named_args, *named_build, *index;
+        item = PyList_GET_ITEM (tuple_names, i);
+        if (item == Py_None) {
+            PyList_Append (format_list, empty_format);
+        } else {
+            named_args = Py_BuildValue ("(O)", item);
+            named_build = PYGLIB_PyUnicode_Format (named_format, named_args);
+            Py_DECREF (named_args);
+            PyList_Append (format_list, named_build);
+            Py_DECREF (named_build);
+            index = PYGLIB_PyLong_FromSsize_t (i);
+            PyDict_SetItem (index_dict, item, index);
+            Py_DECREF (index);
+        }
+    }
+    Py_DECREF (empty_format);
+    Py_DECREF (named_format);
+
+    sep = PYGLIB_PyUnicode_FromString (", ");
+    format_string = PyObject_CallMethod (sep, "join", "O", format_list);
+    Py_DECREF (sep);
+    Py_DECREF (format_list);
+    paren_format = PYGLIB_PyUnicode_FromString ("(%s)");
+    paren_string = PYGLIB_PyUnicode_Format (paren_format, format_string);
+    Py_DECREF (paren_format);
+    Py_DECREF (format_string);
+
+    PyDict_SetItemString (class_dict, repr_format_key, paren_string);
+    Py_DECREF (paren_string);
+
+    PyDict_SetItemString (class_dict, tuple_indices_key, index_dict);
+    Py_DECREF (index_dict);
+
+    new_type_args = Py_BuildValue ("s(O)O", "_ResultTuple",
+                                   &PyGIResultTuple_Type, class_dict);
+    new_type = (PyTypeObject *)PyType_Type.tp_new (&PyType_Type,
+                                                   new_type_args, NULL);
+    Py_DECREF (new_type_args);
+    Py_DECREF (class_dict);
+
+    if (new_type != NULL) {
+        /* disallow subclassing as that would break the free list caching
+         * since we assume that all subclasses use PyTupleObject */
+        new_type->tp_flags &= ~Py_TPFLAGS_BASETYPE;
+    }
+
+    return new_type;
+}
+
+
+/**
+ * pygi_resulttuple_new:
+ * @subclass: A PyGIResultTuple_Type subclass which will be the type of the
+ *    returned instance.
+ * @len: Length of the returned tuple
+ *
+ * Like PyTuple_New(). Return an uninitialized tuple of the given @length.
+ *
+ * Returns: An instance of @subclass or %NULL on error.
+ */
+PyObject *
+pygi_resulttuple_new(PyTypeObject *subclass, Py_ssize_t len) {
+    PyObject *self;
+    Py_ssize_t i;
+
+    /* Check the free list for a tuple object with the needed size;
+     * clear it and change the class to ours.
+     */
+    if (len > 0 && len < PyGIResultTuple_MAXSAVESIZE) {
+        self = free_list[len];
+        if (self != NULL) {
+            free_list[len] = PyTuple_GET_ITEM (self, 0);
+            numfree[len]--;
+            for (i=0; i < len; i++) {
+                PyTuple_SET_ITEM (self, i, NULL);
+            }
+            Py_TYPE (self) = subclass;
+            Py_INCREF (subclass);
+            _Py_NewReference (self);
+            PyObject_GC_Track (self);
+            return self;
+        }
+    }
+
+    /* For zero length tuples and in case the free list is empty, alloc
+     * as usual.
+     */
+    return subclass->tp_alloc (subclass, len);
+}
+
+static void resulttuple_dealloc(PyObject *self) {
+    Py_ssize_t i, len;
+
+    PyObject_GC_UnTrack (self);
+    Py_TRASHCAN_SAFE_BEGIN (self)
+
+    /* Free the tuple items and, if there is space, save the tuple object
+     * pointer to the front of the free list for its size. Otherwise free it.
+     */
+    len = Py_SIZE (self);
+    if (len > 0) {
+        for (i=0; i < len; i++) {
+            Py_XDECREF (PyTuple_GET_ITEM (self, i));
+        }
+
+        if (len < PyGIResultTuple_MAXSAVESIZE && numfree[len] < PyGIResultTuple_MAXFREELIST) {
+            PyTuple_SET_ITEM (self, 0, free_list[len]);
+            numfree[len]++;
+            free_list[len] = self;
+            goto done;
+        }
+    }
+
+    Py_TYPE (self)->tp_free (self);
+
+done:
+    Py_TRASHCAN_SAFE_END (self)
+}
+
+/**
+ * pygi_resulttuple_register_types:
+ * @module: A Python modules to which ResultTuple gets added to.
+ *
+ * Initializes the ResultTuple class and adds it to the passed @module.
+ *
+ * Returns: -1 on error, 0 on success.
+ */
+int pygi_resulttuple_register_types(PyObject *module) {
+
+    PyGIResultTuple_Type.tp_base = &PyTuple_Type;
+    PyGIResultTuple_Type.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
+    PyGIResultTuple_Type.tp_repr = (reprfunc)resulttuple_repr;
+    PyGIResultTuple_Type.tp_getattro = (getattrofunc)resulttuple_getattro;
+    PyGIResultTuple_Type.tp_methods = resulttuple_methods;
+    PyGIResultTuple_Type.tp_dealloc = (destructor)resulttuple_dealloc;
+
+    if (PyType_Ready (&PyGIResultTuple_Type))
+        return -1;
+
+    Py_INCREF (&PyGIResultTuple_Type);
+    if (PyModule_AddObject (module, "ResultTuple",
+                            (PyObject *)&PyGIResultTuple_Type)) {
+        Py_DECREF (&PyGIResultTuple_Type);
+        return -1;
+    }
+
+    return 0;
+}
diff --git a/gi/pygi-resulttuple.h b/gi/pygi-resulttuple.h
new file mode 100644
index 0000000..3f63ca0
--- /dev/null
+++ b/gi/pygi-resulttuple.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C; c-basic-offset: 4 -*-
+ * vim: tabstop=4 shiftwidth=4 expandtab
+ *
+ * Copyright (C) 2015 Christoph Reiter <reiter christoph gmail com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __PYGI_RESULTTUPLE_H__
+#define __PYGI_RESULTTUPLE_H__
+
+#include "Python.h"
+
+int
+pygi_resulttuple_register_types    (PyObject *d);
+
+PyTypeObject *
+pygi_resulttuple_new_type          (PyObject *tuple_names);
+
+PyObject*
+pygi_resulttuple_new               (PyTypeObject *subclass, Py_ssize_t len);
+
+#endif /* __PYGI_RESULTTUPLE_H__ */
diff --git a/gi/pyglib-python-compat.h b/gi/pyglib-python-compat.h
index 58c8cf9..7b67d55 100644
--- a/gi/pyglib-python-compat.h
+++ b/gi/pyglib-python-compat.h
@@ -48,6 +48,7 @@
 #define PYGLIB_PyUnicode_Type PyString_Type
 #define PYGLIB_PyUnicode_InternFromString PyString_InternFromString
 #define PYGLIB_PyUnicode_InternInPlace PyString_InternInPlace
+#define PYGLIB_PyUnicode_Format PyString_Format
 
 #define PYGLIB_PyBytes_FromString PyString_FromString
 #define PYGLIB_PyBytes_FromStringAndSize PyString_FromStringAndSize
@@ -61,6 +62,7 @@
 #define PYGLIB_PyLong_FromSsize_t PyInt_FromSsize_t
 #define PYGLIB_PyLong_FromSize_t PyInt_FromSize_t
 #define PYGLIB_PyLong_AsLong  PyInt_AsLong
+#define PYGLIB_PyLong_AsSsize_t  PyInt_AsSsize_t
 #define PYGLIB_PyLongObject PyIntObject
 #define PYGLIB_PyLong_Type PyInt_Type
 #define PYGLIB_PyLong_AS_LONG PyInt_AS_LONG
@@ -166,11 +168,14 @@ PyTypeObject symbol = {                                 \
 #define PYGLIB_PyUnicode_Type PyUnicode_Type
 #define PYGLIB_PyUnicode_InternFromString PyUnicode_InternFromString
 #define PYGLIB_PyUnicode_InternInPlace PyUnicode_InternInPlace
+#define PYGLIB_PyUnicode_Format PyUnicode_Format
 
 #define PYGLIB_PyLong_Check PyLong_Check
 #define PYGLIB_PyLong_FromLong PyLong_FromLong
+#define PYGLIB_PyLong_FromSsize_t PyLong_FromSsize_t
 #define PYGLIB_PyLong_FromSize_t PyLong_FromSize_t
 #define PYGLIB_PyLong_AsLong PyLong_AsLong
+#define PYGLIB_PyLong_AsSsize_t PyLong_AsSsize_t
 #define PYGLIB_PyLong_AS_LONG(o) PyLong_AS_LONG((PyObject*)(o))
 #define PYGLIB_PyLongObject PyLongObject
 #define PYGLIB_PyLong_Type PyLong_Type
diff --git a/tests/Makefile.am b/tests/Makefile.am
index b36ff51..9d5db5e 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -116,6 +116,7 @@ EXTRA_DIST = \
        test_generictreemodel.py \
        test_docstring.py \
        test_repository.py \
+       test_resulttuple.py \
        compat_test_pygtk.py \
        gi/__init__.py \
        gi/overrides/__init__.py \
diff --git a/tests/test_resulttuple.py b/tests/test_resulttuple.py
new file mode 100644
index 0000000..20f80f3
--- /dev/null
+++ b/tests/test_resulttuple.py
@@ -0,0 +1,87 @@
+# -*- Mode: Python; py-indent-offset: 4 -*-
+# vim: tabstop=4 shiftwidth=4 expandtab
+#
+# Copyright (C) 2015 Christoph Reiter <reiter christoph gmail com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
+# USA
+
+import unittest
+import pickle
+
+import gi
+from gi.repository import GIMarshallingTests
+from gi.repository import Regress
+
+
+ResultTuple = gi._gi.ResultTuple
+
+
+class TestResultTuple(unittest.TestCase):
+
+    def test_base(self):
+        self.assertTrue(issubclass(ResultTuple, tuple))
+
+    def test_create(self):
+        new = ResultTuple._new_type([None, "foo", None, "bar"])
+        self.assertTrue(issubclass(new, ResultTuple))
+
+    def test_repr_dir(self):
+        new = ResultTuple._new_type([None, "foo", None, "bar"])
+        inst = new([1, 2, 3, "a"])
+
+        self.assertEqual(repr(inst), "(1, foo=2, 3, bar='a')")
+        self.assertTrue("foo" in dir(inst))
+
+    def test_repr_dir_empty(self):
+        new = ResultTuple._new_type([])
+        inst = new()
+        self.assertEqual(repr(inst), "()")
+        dir(inst)
+
+    def test_getatttr(self):
+        new = ResultTuple._new_type([None, "foo", None, "bar"])
+        inst = new([1, 2, 3, "a"])
+
+        self.assertTrue(hasattr(inst, "foo"))
+        self.assertEqual(inst.foo, inst[1])
+        self.assertRaises(AttributeError, getattr, inst, "nope")
+
+    def test_pickle(self):
+        new = ResultTuple._new_type([None, "foo", None, "bar"])
+        inst = new([1, 2, 3, "a"])
+
+        inst2 = pickle.loads(pickle.dumps(inst))
+        self.assertEqual(inst2, inst)
+        self.assertTrue(isinstance(inst2, tuple))
+        self.assertFalse(isinstance(inst2, new))
+
+    def test_gi(self):
+        res = GIMarshallingTests.init_function([])
+        self.assertEqual(repr(res), "(True, argv=[])")
+
+        res = GIMarshallingTests.array_return_etc(5, 9)
+        self.assertEqual(repr(res), "([5, 0, 1, 9], sum=14)")
+
+        res = GIMarshallingTests.array_out_etc(-5, 9)
+        self.assertEqual(repr(res), "(ints=[-5, 0, 1, 9], sum=4)")
+
+        cb = lambda: (1, 2)
+        res = GIMarshallingTests.callback_multiple_out_parameters(cb)
+        self.assertEqual(repr(res), "(a=1.0, b=2.0)")
+
+    def test_regress(self):
+        res = Regress.TestObj().skip_return_val(50, 42.0, 60, 2, 3)
+        self.assertEqual(repr(res), "(out_b=51, inout_d=61, out_sum=32)")


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