diff options
author | DongHun Kwak <dh0128.kwak@samsung.com> | 2017-07-12 08:48:42 +0900 |
---|---|---|
committer | DongHun Kwak <dh0128.kwak@samsung.com> | 2017-07-12 08:48:45 +0900 |
commit | 265684897ba0b24e28710a988cab901acbec1775 (patch) | |
tree | 332d43c2c54979ddb5390359f6ab1d865bdda3b6 | |
parent | 01ddc2783a1d721a003f181000e266acde134d19 (diff) | |
download | pygobject2-265684897ba0b24e28710a988cab901acbec1775.tar.gz pygobject2-265684897ba0b24e28710a988cab901acbec1775.tar.bz2 pygobject2-265684897ba0b24e28710a988cab901acbec1775.zip |
Imported Upstream version 3.7.90
Change-Id: I6f093f89379c4f834493e7edf7a32b8c4ad32393
Signed-off-by: DongHun Kwak <dh0128.kwak@samsung.com>
-rw-r--r-- | ChangeLog | 313 | ||||
-rw-r--r-- | NEWS | 17 | ||||
-rw-r--r-- | PKG-INFO | 4 | ||||
-rwxr-xr-x | autogen.sh | 30 | ||||
-rwxr-xr-x | configure | 29 | ||||
-rw-r--r-- | configure.ac | 12 | ||||
-rw-r--r-- | gi/_gobject/gobjectmodule.c | 10 | ||||
-rw-r--r-- | gi/_gobject/pygobject-private.h | 2 | ||||
-rw-r--r-- | gi/_gobject/pygobject.c | 62 | ||||
-rw-r--r-- | gi/_gobject/pygobject.h | 8 | ||||
-rw-r--r-- | gi/overrides/Gtk.py | 42 | ||||
-rw-r--r-- | gi/overrides/__init__.py | 3 | ||||
-rw-r--r-- | gi/pygi-argument.c | 109 | ||||
-rw-r--r-- | gi/pygi-boxed.c | 31 | ||||
-rw-r--r-- | gi/pygi-marshal-from-py.c | 196 | ||||
-rw-r--r-- | gi/pygi-marshal-from-py.h | 14 | ||||
-rw-r--r-- | gi/pygi-marshal-to-py.c | 45 | ||||
-rw-r--r-- | gi/pygi-marshal-to-py.h | 5 | ||||
-rw-r--r-- | gi/pygtkcompat.py | 2 | ||||
-rw-r--r-- | gi/types.py | 6 | ||||
-rw-r--r-- | pygtkcompat/Makefile.am | 1 | ||||
-rw-r--r-- | pygtkcompat/Makefile.in | 1 | ||||
-rw-r--r-- | pygtkcompat/generictreemodel.py | 420 | ||||
-rw-r--r-- | pygtkcompat/pygtkcompat.py | 3 | ||||
-rw-r--r-- | tests/Makefile.am | 2 | ||||
-rw-r--r-- | tests/Makefile.in | 2 | ||||
-rw-r--r-- | tests/test_generictreemodel.py | 406 | ||||
-rw-r--r-- | tests/test_gi.py | 6 | ||||
-rw-r--r-- | tests/test_object_marshaling.py | 616 | ||||
-rw-r--r-- | tests/test_overrides_gtk.py | 123 |
30 files changed, 2241 insertions, 279 deletions
@@ -1,3 +1,316 @@ +commit f2fb7f6142cd7112db9c2526d7f1c642a50cfc2a +Author: Martin Pitt <martinpitt@gnome.org> +Date: Tue Feb 19 12:19:35 2013 +0100 + + Release 3.7.90 + + NEWS | 17 +++++++++++++++++ + configure.ac | 2 +- + 2 files changed, 18 insertions(+), 1 deletion(-) + +commit 840c871441cb215f24cc6e7ed26b9f38e5aad0df +Author: Simon Feltman <sfeltman@src.gnome.org> +Date: Mon Feb 18 01:46:22 2013 -0800 + + overrides: Fix inconsistencies with drag and drop target list API + + Add support to Gtk.Widget.drag_dest_set_target_list and + Gtk.Widget.drag_source_set_target_list to accept iterables containing + mixed TargetEntry or a tuple of (target, flags, info). + Add support to Gtk.TreeView.enable_model_drag_source and + Gtk.TreeView.enable_model_drag_dest to accept a list of + Gtk.TargetEntry + items. + + https://bugzilla.gnome.org/show_bug.cgi?id=680640 + + gi/overrides/Gtk.py | 40 +++++++++++++++++++++++++++++----------- + tests/test_overrides_gtk.py | 32 ++++++++++++++++++++++++++++++++ + 2 files changed, 61 insertions(+), 11 deletions(-) + +commit 62e94b0f87845bb7a1cfddf70dcdc89ff7a80bf7 +Author: Simon Feltman <sfeltman@src.gnome.org> +Date: Mon Feb 18 03:19:34 2013 -0800 + + tests: Add test_marshaling_object to Makefile.am + + tests/Makefile.am | 1 + + 1 file changed, 1 insertion(+) + +commit a10fb7216de57046d1ecacb73dd032eaadcbad09 +Author: Simon Feltman <s.feltman@gmail.com> +Date: Wed Aug 29 03:46:23 2012 -0700 + + pygtkcompat: Add pygtk compatible GenericTreeModel implementation + + Add Python implementation of the GenericTreeModel that was + available in pygtk. The implementation attempts a better job + than the original at ref counting by guaranteeing no leaks + upon deletion of the model itself. Or by using the extra "node" + argument to the row_deleted signal. The model is available in + the pygtkcompat package directly as + pygtkcompat.generictreemodel.GenericTreeModel or with as + gtk.GenericTreeModel when pygtkcompat.enable_gtk() is set. + + Add file list and tree demos making use of GenericTreeModel + to gtk-demo. + + Auto-expand gtk-demo app tree to give a better overview of + the demos available. + + https://bugzilla.gnome.org/show_bug.cgi?id=682933 + + .../gtk-demo/demos/Tree View/treemodel_filelist.py | 234 ++++++++++++ + .../gtk-demo/demos/Tree View/treemodel_filetree.py | 279 ++++++++++++++ + demos/gtk-demo/gtk-demo.py | 2 +- + gi/pygtkcompat.py | 2 +- + pygtkcompat/Makefile.am | 1 + + pygtkcompat/generictreemodel.py | 420 + +++++++++++++++++++++ + pygtkcompat/pygtkcompat.py | 3 + + tests/Makefile.am | 1 + + tests/test_generictreemodel.py | 406 + ++++++++++++++++++++ + 9 files changed, 1346 insertions(+), 2 deletions(-) + +commit 871878c7a1e18fbdbf0744e0dd52cbcc6b610cdb +Author: Simon Feltman <sfeltman@src.gnome.org> +Date: Mon Feb 18 02:54:14 2013 -0800 + + overrides: Add support for iterables besides tuples for TreePath + creation + + Allow Gtk.TreePath to accept any iterable for creation of the path. + + https://bugzilla.gnome.org/show_bug.cgi?id=682933 + + gi/overrides/Gtk.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit 93c1536b45f56c20b6d874c41c4cacd2b6cdca0a +Author: Simon Feltman <sfeltman@src.gnome.org> +Date: Fri Feb 15 22:56:29 2013 -0800 + + Unify Python callable to GClosure GI marshaling code + + Add pygi_marshal_from_py_gclosure which can be used for direct + gi method + call args and vfunc out args. + + https://bugzilla.gnome.org/show_bug.cgi?id=693405 + + gi/pygi-argument.c | 14 +----------- + gi/pygi-marshal-from-py.c | 55 + ++++++++++++++++++++++++++++------------------- + gi/pygi-marshal-from-py.h | 3 +++ + 3 files changed, 37 insertions(+), 35 deletions(-) + +commit 9e47afe459df942d9ffc4f71b39f1443976293df +Author: Simon Feltman <sfeltman@src.gnome.org> +Date: Fri Feb 15 20:56:12 2013 -0800 + + Unify Python object to GValue GI marshaling code + + Add pygi_marshal_from_py_g_value which can be used for direct + gi method + call args and vfunc out args. The new method also adds an + "is_allocated" + parameter that will be used to fix leaks in the future. + + https://bugzilla.gnome.org/show_bug.cgi?id=693405 + + gi/pygi-argument.c | 43 +++++++-------------------- + gi/pygi-marshal-from-py.c | 74 + ++++++++++++++++++++++++++++++++--------------- + gi/pygi-marshal-from-py.h | 5 ++++ + 3 files changed, 65 insertions(+), 57 deletions(-) + +commit 15cd7be5ad80e2411d6c13b04f5e2c33e4f5605e +Author: Simon Feltman <sfeltman@src.gnome.org> +Date: Fri Feb 15 23:07:57 2013 -0800 + + Rename pygi_marshal_from_py_object to make it more explicit + + Rename pygi_marshal_from_py_object to pygi_marshal_from_py_gobject + to make it more explicit and give consistency with future refactoring. + + https://bugzilla.gnome.org/show_bug.cgi?id=693405 + + gi/pygi-argument.c | 2 +- + gi/pygi-marshal-from-py.c | 22 +++++++++++++--------- + gi/pygi-marshal-from-py.h | 6 +++--- + 3 files changed, 17 insertions(+), 13 deletions(-) + +commit 84103dfabd05742d1a18729663a609e9bf7c45f8 +Author: Niklas Koep <niklas.koep@gmail.com> +Date: Fri Feb 15 21:23:01 2013 -0800 + + Prefix __module__ attribute of function objects with gi.repository + + This allows gi module methods to work with pydoc and help(). + Additionally correct typo in two docstrings of the same module. + + Co-authored-by: Simon Feltman <sfeltman@src.gnome.org> + + https://bugzilla.gnome.org/show_bug.cgi?id=693839 + + gi/overrides/__init__.py | 3 ++- + gi/types.py | 6 +++--- + 2 files changed, 5 insertions(+), 4 deletions(-) + +commit f6d4d2da676ae63d7a24dd172775b488ce665fe4 +Author: Jonathan Ballet <jon@multani.info> +Date: Thu Feb 14 07:50:02 2013 +0100 + + configure.ac: only enable code coverage when available + + When building with an older gnome-common which does not yet provide + code + coverage support, disable it instead of breaking the configure script. + + https://bugzilla.gnome.org/show_bug.cgi?id=693328 + + configure.ac | 10 +++++++++- + 1 file changed, 9 insertions(+), 1 deletion(-) + +commit 42cbff60e2032f715d9be6ab280954211899e03c +Author: Jonathan Ballet <jon@multani.info> +Date: Tue Feb 12 23:03:00 2013 +0100 + + Correctly set properties on object with statically defined properties + + Fix failures in GObject.Object.set_properties() when used with + statically defined properties: + + * Calling the method was raising a "SystemError: error return without + exception set" since `result` was (most of the time) still NULL at the + end of pygobject_set_properties() + + * Calling the method with several properties would only set one of + the properties, since the function was exiting too early. + + Signed-off-by: Simon Feltman <sfeltman@src.gnome.org> + + https://bugzilla.gnome.org/show_bug.cgi?id=693618 + + gi/_gobject/pygobject.c | 19 +++++++++++-------- + tests/test_gi.py | 6 ++++++ + 2 files changed, 17 insertions(+), 8 deletions(-) + +commit 2384769810a61d6ed08d8742b7ae976ebfaa8cb5 +Author: Martin Pitt <martinpitt@gnome.org> +Date: Mon Feb 11 18:08:37 2013 +0100 + + autogen.sh: Use gnome-autogen.sh + + We depend on gnome-common now anyway, so use gnome-autogen.sh. This + will result + in a much better error message when gnome-common is not installed, + too. + + https://bugzilla.gnome.org/show_bug.cgi?id=693328 + + autogen.sh | 30 +++++++++++++++++------------- + 1 file changed, 17 insertions(+), 13 deletions(-) + +commit c107bb1f9275a748b494d3f32818f227e07cadf0 +Author: Christoph Reiter <christoph.reiter@gmx.at> +Date: Mon Feb 11 10:07:47 2013 +0100 + + GTK tests: Add and use context manager for realized widgets + + https://bugzilla.gnome.org/show_bug.cgi?id=693377 + + tests/test_overrides_gtk.py | 91 + +++++++++++++++++++++++++-------------------- + 1 file changed, 50 insertions(+), 41 deletions(-) + +commit e6670ee26b7682e6213f71deef813ce2e7cd6730 +Author: Martin Pitt <martinpitt@gnome.org> +Date: Mon Feb 11 08:55:19 2013 +0100 + + _pygi_marshal_from_py_array: Fix uninitialized variable + + gi/pygi-marshal-from-py.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit a37bfdcb3d9dcc8bcdd8126ad55d80fab4729c62 +Author: Christoph Reiter <christoph.reiter@gmx.at> +Date: Mon Feb 11 08:34:42 2013 +0100 + + Skip some vfunc tests with gi 1.34 + + https://bugzilla.gnome.org/show_bug.cgi?id=693374 + + tests/test_object_marshaling.py | 14 ++++++++++++++ + 1 file changed, 14 insertions(+) + +commit aff2ea1b681c3019f7dbdc841c2e33de78dbb88f +Author: Simon Feltman <sfeltman@src.gnome.org> +Date: Sun Feb 10 13:40:45 2013 -0800 + + Remove workaround for g_struct_info_get_size reporting incorrect size + + Remove workaround for g_struct_info_get_size reporting incorrect size + for boxed GValues. Verified this now returns the correct size of 24. + + https://bugzilla.gnome.org/show_bug.cgi?id=622711 + + gi/pygi-boxed.c | 31 +++++++++++++------------------ + 1 file changed, 13 insertions(+), 18 deletions(-) + +commit 5efe2e5c8458d9f4d72329ea1209d96b5ebecfb4 +Author: Simon Feltman <sfeltman@src.gnome.org> +Date: Mon Feb 4 20:50:10 2013 -0800 + + Fix reference leaks with transient floating objects + + Unify and refactor caller and callee GObject argument marshalers. + Combine code from the large switch statement used to marshal + arguments to and from vfuncs/closures with the marshalers used + for direct calls to gi functions. This fixes a reference leak + when marshalling GObjects to Python with transfer=full due to + the diverging code paths. + Replace ability in gobject_new_full to optionally sink objects + with ability to optionaly "steal" objects. This fits the premise + that binding layers should always sink objects initially. The + steal argument is then used for marshalling arguments which are + transfer=full. + Add hacks and comments to work around GTK+ bugs 693393 and 693400. + + https://bugzilla.gnome.org/show_bug.cgi?id=687522 + + gi/_gobject/gobjectmodule.c | 10 +++++-- + gi/_gobject/pygobject-private.h | 2 +- + gi/_gobject/pygobject.c | 45 ++++++++++++++++++++---------- + gi/_gobject/pygobject.h | 8 ++++-- + gi/pygi-argument.c | 52 +++++++++++++++++------------------ + gi/pygi-marshal-from-py.c | 61 + +++++++++++++++++++++++++++++++++++++---- + gi/pygi-marshal-from-py.h | 6 ++++ + gi/pygi-marshal-to-py.c | 45 +++++++++++++++--------------- + gi/pygi-marshal-to-py.h | 5 ++++ + tests/test_object_marshaling.py | 44 ++++++++++++++--------------- + 10 files changed, 181 insertions(+), 97 deletions(-) + +commit bd54b8ab30fc957849e7f57e9ee4c4b41aa37013 +Author: Simon Feltman <sfeltman@src.gnome.org> +Date: Wed Feb 6 12:56:44 2013 -0800 + + tests: Fix spelling mistakes in new vfunc object marshalling tests + + tests/test_object_marshaling.py | 12 ++++++------ + 1 file changed, 6 insertions(+), 6 deletions(-) + +commit cd96fd8b8e10add9890f36ec237bb78548de7002 +Author: Martin Pitt <martinpitt@gnome.org> +Date: Tue Feb 5 07:53:38 2013 +0100 + + configure.ac: post-release bump to 3.7.6 + + configure.ac | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + commit 92b1404e61f46348168f32720eff4a482531e5e3 Author: Martin Pitt <martinpitt@gnome.org> Date: Tue Feb 5 07:46:46 2013 +0100 @@ -1,3 +1,20 @@ +3.7.90 19-Feb-2013 + - overrides: Fix inconsistencies with drag and drop target list API + (Simon Feltman) (#680640) + - pygtkcompat: Add pygtk compatible GenericTreeModel implementation + (Simon Feltman) (#682933) + - overrides: Add support for iterables besides tuples for TreePath + creation (Simon Feltman) (#682933) + - Prefix __module__ attribute of function objects with gi.repository + (Niklas Koep) (#693839) + - configure.ac: only enable code coverage when available, to fix + autogen.sh with older gnome-commons (Jonathan Ballet) (#693328) + - Correctly set properties on object with statically defined properties + (Jonathan Ballet) (#693618) + - autogen.sh: Use gnome-autogen.sh (Martin Pitt) (#693328) + - Fix reference leaks with transient floating objects (Simon Feltman) + (#687522) + 3.7.5.1 05-Feb-2013 - Fix ABI break with pygobject.h from 3.7.5 (Simon Feltman) (#675726) @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: PyGObject -Version: 3.7.5.1 +Version: 3.7.90 Summary: Python bindings for GObject Home-page: http://www.pygtk.org/ Author: James Henstridge @@ -8,7 +8,7 @@ Author-email: james@daa.com.au Maintainer: Johan Dahlin Maintainer-email: johan@gnome.org License: GNU LGPL -Download-url: ftp://ftp.gnome.org/pub/GNOME/sources/pygobject/3.7/pygobject-3.7.5.1.tar.gz +Download-url: ftp://ftp.gnome.org/pub/GNOME/sources/pygobject/3.7/pygobject-3.7.90.tar.gz Description: Python bindings for GLib and GObject Platform: POSIX, Windows Classifier: Development Status :: 5 - Production/Stable @@ -1,17 +1,17 @@ #!/bin/sh # Run this to generate all the initial makefiles, etc. +set -e +srcdir=`dirname $0` +test -z "$srcdir" && srcdir=. -test -n "$srcdir" || srcdir=`dirname "$0"` -test -n "$srcdir" || srcdir=. +PKG_NAME="pygobject" -olddir=`pwd` -cd "$srcdir" - -AUTORECONF=`which autoreconf` -if test -z $AUTORECONF; then - echo "*** No autoreconf found, please install it ***" - exit 1 -fi +(test -f $srcdir/configure.ac \ + && test -f $srcdir/$PKG_NAME.doap) || { + echo -n "**Error**: Directory "\`$srcdir\'" does not look like the" + echo " top-level $PKG_NAME directory" + exit 1 +} if type lcov >/dev/null 2>&1; then EXTRA_ARGS="--enable-code-coverage" @@ -19,7 +19,11 @@ else echo "lcov not installed, not enabling code coverage" fi -autoreconf --force --install --verbose || exit $? -cd "$olddir" -test -n "$NOCONFIGURE" || "$srcdir/configure" $EXTRA_ARGS "$@" +which gnome-autogen.sh || { + echo "You need to install gnome-common." + exit 1 +} + +gnome-autogen.sh "$EXTRA_ARGS" "$@" + @@ -1,6 +1,6 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.69 for pygobject 3.7.5.1. +# Generated by GNU Autoconf 2.69 for pygobject 3.7.90. # # Report bugs to <http://bugzilla.gnome.org/enter_bug.cgi?product=pygobject>. # @@ -591,8 +591,8 @@ MAKEFLAGS= # Identity of this package. PACKAGE_NAME='pygobject' PACKAGE_TARNAME='pygobject' -PACKAGE_VERSION='3.7.5.1' -PACKAGE_STRING='pygobject 3.7.5.1' +PACKAGE_VERSION='3.7.90' +PACKAGE_STRING='pygobject 3.7.90' PACKAGE_BUGREPORT='http://bugzilla.gnome.org/enter_bug.cgi?product=pygobject' PACKAGE_URL='https://live.gnome.org/PyGObject/' @@ -1394,7 +1394,7 @@ if test "$ac_init_help" = "long"; then # Omit some internal or obsolete options to make the list less imposing. # This message is too long to be a string in the A/UX 3.1 sh. cat <<_ACEOF -\`configure' configures pygobject 3.7.5.1 to adapt to many kinds of systems. +\`configure' configures pygobject 3.7.90 to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... @@ -1464,7 +1464,7 @@ fi if test -n "$ac_init_help"; then case $ac_init_help in - short | recursive ) echo "Configuration of pygobject 3.7.5.1:";; + short | recursive ) echo "Configuration of pygobject 3.7.90:";; esac cat <<\_ACEOF @@ -1599,7 +1599,7 @@ fi test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF -pygobject configure 3.7.5.1 +pygobject configure 3.7.90 generated by GNU Autoconf 2.69 Copyright (C) 2012 Free Software Foundation, Inc. @@ -1877,7 +1877,7 @@ cat >config.log <<_ACEOF This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. -It was created by pygobject $as_me 3.7.5.1, which was +It was created by pygobject $as_me 3.7.90, which was generated by GNU Autoconf 2.69. Invocation command line was $ $0 $@ @@ -2241,9 +2241,9 @@ $as_echo "#define PYGOBJECT_MINOR_VERSION 7" >>confdefs.h PYGOBJECT_MINOR_VERSION=7 -$as_echo "#define PYGOBJECT_MICRO_VERSION 5.1" >>confdefs.h +$as_echo "#define PYGOBJECT_MICRO_VERSION 90" >>confdefs.h -PYGOBJECT_MICRO_VERSION=5.1 +PYGOBJECT_MICRO_VERSION=90 ac_config_headers="$ac_config_headers config.h" @@ -2754,7 +2754,7 @@ fi # Define the identity of the package. PACKAGE='pygobject' - VERSION='3.7.5.1' + VERSION='3.7.90' cat >>confdefs.h <<_ACEOF @@ -14327,6 +14327,10 @@ $as_echo "$complCFLAGS" >&6; } WARN_CFLAGS="$tested_warning_flags $complCFLAGS" +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for Gnome code coverage support" >&5 +$as_echo_n "checking for Gnome code coverage support... " >&6; } +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +$as_echo "yes" >&6; } { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to build with code coverage support" >&5 $as_echo_n "checking whether to build with code coverage support... " >&6; } @@ -14561,6 +14565,7 @@ DISTCHECK_CONFIGURE_FLAGS += --disable-code-coverage + if test "x$GCC" = "xyes"; then case " $CFLAGS " in @@ -15255,7 +15260,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log=" -This file was extended by pygobject $as_me 3.7.5.1, which was +This file was extended by pygobject $as_me 3.7.90, which was generated by GNU Autoconf 2.69. Invocation command line was CONFIG_FILES = $CONFIG_FILES @@ -15322,7 +15327,7 @@ _ACEOF cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`" ac_cs_version="\\ -pygobject config.status 3.7.5.1 +pygobject config.status 3.7.90 configured by $0, generated by GNU Autoconf 2.69, with options \\"\$ac_cs_config\\" diff --git a/configure.ac b/configure.ac index 3507f4f..a1b43cd 100644 --- a/configure.ac +++ b/configure.ac @@ -18,7 +18,7 @@ m4_define(python3_min_ver, 3.1) dnl the pygobject version number m4_define(pygobject_major_version, 3) m4_define(pygobject_minor_version, 7) -m4_define(pygobject_micro_version, 5.1) +m4_define(pygobject_micro_version, 90) m4_define(pygobject_version, pygobject_major_version.pygobject_minor_version.pygobject_micro_version) dnl versions of packages we require ... @@ -228,7 +228,15 @@ AC_SUBST(INTROSPECTION_COMPILER) # compiler warnings, errors, required cflags, and code coverage support GNOME_COMPILE_WARNINGS([maximum]) -GNOME_CODE_COVERAGE +AC_MSG_CHECKING(for Gnome code coverage support) +m4_ifdef([GNOME_CODE_COVERAGE], + [AC_MSG_RESULT(yes) + GNOME_CODE_COVERAGE], + [AC_MSG_RESULT(no) + GNOME_CODE_COVERAGE_RULES='' + AC_SUBST([GNOME_CODE_COVERAGE_RULES]) + enable_code_coverage="no"]) + if test "x$GCC" = "xyes"; then JH_ADD_CFLAG([-Wall]) JH_ADD_CFLAG([-Werror=unused-variable]) diff --git a/gi/_gobject/gobjectmodule.c b/gi/_gobject/gobjectmodule.c index 96b5c94..fda21e7 100644 --- a/gi/_gobject/gobjectmodule.c +++ b/gi/_gobject/gobjectmodule.c @@ -996,7 +996,11 @@ pygobject_constructv(PyGObject *self, pygobject_init_wrapper_set((PyObject *) self); obj = g_object_newv(pyg_type_from_object((PyObject *) self), n_parameters, parameters); + + if (g_object_is_floating (obj)) + self->private_flags.flags |= PYGOBJECT_GOBJECT_WAS_FLOATING; pygobject_sink (obj); + pygobject_init_wrapper_set(NULL); if (self->obj == NULL) { self->obj = obj; @@ -1034,7 +1038,9 @@ pygobject__g_instance_init(GTypeInstance *instance, * now */ PyGILState_STATE state; state = pyglib_gil_state_ensure(); - wrapper = pygobject_new_full(object, TRUE, g_class); + wrapper = pygobject_new_full(object, + /*steal=*/ FALSE, + g_class); /* float the wrapper ref here because we are going to orphan it * so we don't destroy the wrapper. The next call to pygobject_new_full @@ -1477,7 +1483,7 @@ pyg_object_new (PyGObject *self, PyObject *args, PyObject *kwargs) if (obj) { pygobject_sink (obj); - self = (PyGObject *) pygobject_new_full((GObject *)obj, TRUE, NULL); + self = (PyGObject *) pygobject_new((GObject *)obj); g_object_unref(obj); } else self = NULL; diff --git a/gi/_gobject/pygobject-private.h b/gi/_gobject/pygobject-private.h index 1b1d6db..0d1aca0 100644 --- a/gi/_gobject/pygobject-private.h +++ b/gi/_gobject/pygobject-private.h @@ -154,7 +154,7 @@ void pygobject_register_class (PyObject *dict, PyObject *bases); void pygobject_register_wrapper (PyObject *self); PyObject * pygobject_new (GObject *obj); -PyObject * pygobject_new_full (GObject *obj, gboolean sink, gpointer g_class); +PyObject * pygobject_new_full (GObject *obj, gboolean steal, gpointer g_class); void pygobject_sink (GObject *obj); PyTypeObject *pygobject_lookup_class (GType gtype); void pygobject_watch_closure (PyObject *self, GClosure *closure); diff --git a/gi/_gobject/pygobject.c b/gi/_gobject/pygobject.c index a23d469..8673e79 100644 --- a/gi/_gobject/pygobject.c +++ b/gi/_gobject/pygobject.c @@ -951,7 +951,7 @@ pygobject_lookup_class(GType gtype) /** * pygobject_new_full: * @obj: a GObject instance. - * @sink: whether to sink any floating reference found on the GObject. + * @steal: whether to steal a ref from the GObject or add (sink) a new one. * @g_class: the GObjectClass * * This function gets a reference to a wrapper for the given GObject @@ -962,19 +962,30 @@ pygobject_lookup_class(GType gtype) * Returns: a reference to the wrapper for the GObject. */ PyObject * -pygobject_new_full(GObject *obj, gboolean sink, gpointer g_class) +pygobject_new_full(GObject *obj, gboolean steal, gpointer g_class) { PyGObject *self; if (obj == NULL) { - Py_INCREF(Py_None); - return Py_None; + Py_RETURN_NONE; } - /* we already have a wrapper for this object -- return it. */ + /* If the GObject already has a PyObject wrapper stashed in its qdata, re-use it. + */ self = (PyGObject *)g_object_get_qdata(obj, pygobject_wrapper_key); if (self != NULL) { - pygobject_ref_sink(self); + /* Note the use of "pygobject_ref_sink" here only deals with PyObject + * wrapper ref counts and has nothing to do with GObject. + */ + pygobject_ref_sink(self); + + /* If steal is true, we also want to decref the incoming GObjects which + * already have a Python wrapper because the wrapper is already holding a + * strong reference. + */ + if (steal) + g_object_unref (obj); + } else { /* create wrapper */ PyGObjectData *inst_data = pyg_object_peek_inst_data(obj); @@ -1000,13 +1011,15 @@ pygobject_new_full(GObject *obj, gboolean sink, gpointer g_class) self->weakreflist = NULL; self->private_flags.flags = 0; self->obj = obj; - /* if we are creating a wrapper around a newly created object, it can have - a floating ref (e.g. for methods like Gtk.Button.new()). Bug 640868 */ - if (sink) - g_object_ref_sink(obj); - else - g_object_ref(obj); - pygobject_register_wrapper((PyObject *)self); + + /* If we are not stealing a ref or the object is floating, + * add a regular ref or sink the object. */ + if (g_object_is_floating (obj)) + self->private_flags.flags |= PYGOBJECT_GOBJECT_WAS_FLOATING; + if (!steal || self->private_flags.flags & PYGOBJECT_GOBJECT_WAS_FLOATING) + g_object_ref_sink (obj); + + pygobject_register_wrapper((PyObject *)self); PyObject_GC_Track((PyObject *)self); } @@ -1017,7 +1030,9 @@ pygobject_new_full(GObject *obj, gboolean sink, gpointer g_class) PyObject * pygobject_new(GObject *obj) { - return pygobject_new_full(obj, TRUE, NULL); + return pygobject_new_full(obj, + /*steal=*/FALSE, + NULL); } static void @@ -1438,14 +1453,17 @@ pygobject_set_properties(PyGObject *self, PyObject *args, PyObject *kwargs) goto exit; } - ret = pygi_set_property_value (self, pspec, value); - if (ret == 0) - goto exit; - else if (PyErr_Occurred()) - goto exit; + ret = pygi_set_property_value (self, pspec, value); + if (ret != 0) { + /* Non-zero return code means that either an error occured ...*/ + if (PyErr_Occurred()) + goto exit; - if (!set_property_from_pspec(G_OBJECT(self->obj), pspec, value)) - goto exit; + /* ... or the property couldn't be found , so let's try the default + * call. */ + if (!set_property_from_pspec(G_OBJECT(self->obj), pspec, value)) + goto exit; + } } result = Py_None; @@ -2343,7 +2361,7 @@ pygobject_weak_ref_call(PyGObjectWeakRef *self, PyObject *args, PyObject *kw) return NULL; if (self->obj) - return pygobject_new_full(self->obj, FALSE, NULL); + return pygobject_new(self->obj); else { Py_INCREF(Py_None); return Py_None; diff --git a/gi/_gobject/pygobject.h b/gi/_gobject/pygobject.h index ba40986..92dc030 100644 --- a/gi/_gobject/pygobject.h +++ b/gi/_gobject/pygobject.h @@ -24,7 +24,8 @@ struct _PyGClosure { typedef enum { PYGOBJECT_USING_TOGGLE_REF = 1 << 0, - PYGOBJECT_IS_FLOATING_REF = 1 << 1 + PYGOBJECT_IS_FLOATING_REF = 1 << 1, + PYGOBJECT_GOBJECT_WAS_FLOATING = 1 << 2, } PyGObjectFlags; /* closures is just an alias for what is found in the @@ -187,8 +188,9 @@ struct _PyGObject_Functions { gpointer data); gboolean (*gerror_exception_check) (GError **error); PyObject* (*option_group_new) (GOptionGroup *group); - GType (* type_from_object_strict) (PyObject *obj, gboolean strict); - PyObject *(* newgobj_full)(GObject *obj, gboolean sink, gpointer g_class); + GType (* type_from_object_strict) (PyObject *obj, gboolean strict); + + PyObject *(* newgobj_full)(GObject *obj, gboolean steal, gpointer g_class); }; #ifndef _INSIDE_PYGOBJECT_ diff --git a/gi/overrides/Gtk.py b/gi/overrides/Gtk.py index 25f127f..002f22b 100644 --- a/gi/overrides/Gtk.py +++ b/gi/overrides/Gtk.py @@ -48,6 +48,22 @@ python module to use with Gtk 2.0" warnings.warn(warn_msg, RuntimeWarning) +def _construct_target_list(targets): + """Create a list of TargetEntry items from a list of tuples in the form (target, flags, info) + + The list can also contain existing TargetEntry items in which case the existing entry + is re-used in the return list. + """ + target_entries = [] + for entry in targets: + if not isinstance(entry, Gtk.TargetEntry): + entry = Gtk.TargetEntry.new(*entry) + target_entries.append(entry) + return target_entries + +__all__.append('_construct_target_list') + + class Widget(Gtk.Widget): def translate_coordinates(self, dest_widget, src_x, src_y): @@ -59,6 +75,17 @@ class Widget(Gtk.Widget): def render_icon(self, stock_id, size, detail=None): return super(Widget, self).render_icon(stock_id, size, detail) + def drag_dest_set_target_list(self, target_list): + if not isinstance(target_list, Gtk.TargetList): + target_list = Gtk.TargetList.new(_construct_target_list(target_list)) + super(Widget, self).drag_dest_set_target_list(target_list) + + def drag_source_set_target_list(self, target_list): + if not isinstance(target_list, Gtk.TargetList): + target_list = Gtk.TargetList.new(_construct_target_list(target_list)) + super(Widget, self).drag_source_set_target_list(target_list) + + Widget = override(Widget) __all__.append('Widget') @@ -1160,7 +1187,7 @@ class TreePath(Gtk.TreePath): def __new__(cls, path=0): if isinstance(path, int): path = str(path) - elif isinstance(path, tuple): + elif not isinstance(path, _basestring): path = ":".join(str(val) for val in path) if len(path) == 0: @@ -1306,23 +1333,14 @@ class TreeView(Gtk.TreeView, Container): if success: return (path, pos,) - def _construct_target_list(self, targets): - # FIXME: this should most likely be part of Widget or a global helper - # function - target_entries = [] - for t in targets: - entry = Gtk.TargetEntry.new(*t) - target_entries.append(entry) - return target_entries - def enable_model_drag_source(self, start_button_mask, targets, actions): - target_entries = self._construct_target_list(targets) + target_entries = _construct_target_list(targets) super(TreeView, self).enable_model_drag_source(start_button_mask, target_entries, actions) def enable_model_drag_dest(self, targets, actions): - target_entries = self._construct_target_list(targets) + target_entries = _construct_target_list(targets) super(TreeView, self).enable_model_drag_dest(target_entries, actions) diff --git a/gi/overrides/__init__.py b/gi/overrides/__init__.py index b7d365c..0bd94b8 100644 --- a/gi/overrides/__init__.py +++ b/gi/overrides/__init__.py @@ -55,7 +55,8 @@ class overridefunc(object): if not hasattr(func, '__info__'): raise TypeError("func must be an gi function") from ..importer import modules - self.module = modules[func.__module__]._introspection_module + module_name = func.__module__.rsplit('.', 1)[-1] + self.module = modules[module_name]._introspection_module def __call__(self, func): def wrapper(*args, **kwargs): diff --git a/gi/pygi-argument.c b/gi/pygi-argument.c index 53e0b5c..3b65246 100644 --- a/gi/pygi-argument.c +++ b/gi/pygi-argument.c @@ -31,6 +31,10 @@ #include <pyglib-python-compat.h> #include <pyglib.h> +#include "pygi-marshal-from-py.h" +#include "pygi-marshal-to-py.h" + + static gboolean gi_argument_to_gssize (GIArgument *arg_in, GITypeTag type_tag, @@ -1235,55 +1239,20 @@ array_success: /* Handle special cases first. */ if (g_type_is_a (type, G_TYPE_VALUE)) { - GValue *value; - GType object_type; - gint retval; - - object_type = pyg_type_from_object_strict ( (PyObject *) object->ob_type, FALSE); - if (object_type == G_TYPE_INVALID) { - PyErr_SetString (PyExc_RuntimeError, "unable to retrieve object's GType"); - break; - } - g_warn_if_fail (transfer == GI_TRANSFER_NOTHING); + /* This will currently leak the GValue that is allocated and + * stashed in arg.v_pointer. Out argument marshaling for caller + * allocated GValues already pass in memory for the GValue. + * Further re-factoring is needed to fix this leak. + * See: https://bugzilla.gnome.org/show_bug.cgi?id=693405 + */ + pygi_marshal_from_py_gvalue (object, + &arg, + transfer, + FALSE /*is_allocated*/); - value = g_slice_new0 (GValue); - - /* if already a gvalue, copy, else marshal into gvalue */ - if (object_type == G_TYPE_VALUE) { - /* src GValue's lifecycle is handled by Python - * so we have to copy it into the destination's - * GValue which is freed during the cleanup of - * invoke. - */ - GValue *src = (GValue *)((PyGObject *) object)->obj; - g_value_init (value, G_VALUE_TYPE (src)); - g_value_copy(src, value); - } else { - g_value_init (value, object_type); - retval = pyg_value_from_pyobject (value, object); - if (retval < 0) { - g_slice_free (GValue, value); - PyErr_SetString (PyExc_RuntimeError, "PyObject conversion to GValue failed"); - break; - } - } - - arg.v_pointer = value; } else if (g_type_is_a (type, G_TYPE_CLOSURE)) { - GClosure *closure; - - if (pyg_type_from_object_strict (object, FALSE) == G_TYPE_CLOSURE) { - closure = (GClosure *)pyg_boxed_get (object, void); - } else { - closure = pyg_closure_new (object, NULL, NULL); - if (closure == NULL) { - PyErr_SetString (PyExc_RuntimeError, "PyObject conversion to GClosure failed"); - break; - } - } - - arg.v_pointer = closure; + pygi_marshal_from_py_gclosure (object, &arg); } else if (g_struct_info_is_foreign (info)) { pygi_struct_foreign_convert_to_g_argument (object, info, transfer, &arg); } else if (g_type_is_a (type, G_TYPE_BOXED)) { @@ -1329,17 +1298,10 @@ array_success: } case GI_INFO_TYPE_INTERFACE: case GI_INFO_TYPE_OBJECT: - if (object == Py_None) { - arg.v_pointer = NULL; - break; - } - - arg.v_pointer = pygobject_get (object); - if (transfer == GI_TRANSFER_EVERYTHING) { - g_object_ref (arg.v_pointer); - } - + /* An error within this call will result in a NULL arg */ + pygi_marshal_from_py_gobject (object, &arg, transfer); break; + default: g_assert_not_reached(); } @@ -1856,23 +1818,26 @@ _pygi_argument_to_object (GIArgument *arg, } case GI_INFO_TYPE_INTERFACE: case GI_INFO_TYPE_OBJECT: - if (arg->v_pointer == NULL) { - object = Py_None; - Py_INCREF (object); - break; - } - - if (G_IS_PARAM_SPEC (arg->v_pointer)) { - object = pyg_param_spec_new (arg->v_pointer); - break; - } - - /* Only sink incoming objects if the transfer everything. - * See: https://bugzilla.gnome.org/show_bug.cgi?id=675726 + /* HACK: + * The following hack is to work around GTK+ sending signals which + * contain floating widgets in them. This assumes control of how + * references are added by the PyGObject wrapper and avoids the sink + * behavior by explicitly passing GI_TRANSFER_EVERYTHING as the transfer + * mode and then re-forcing the object as floating afterwards. + * + * This can be deleted once the following ticket is fixed: + * https://bugzilla.gnome.org/show_bug.cgi?id=693400 */ - object = pygobject_new_full (arg->v_pointer, - /*sink=*/ transfer == GI_TRANSFER_EVERYTHING, - /*type=*/ NULL); + if (arg->v_pointer && + !G_IS_PARAM_SPEC (arg->v_pointer) && + transfer == GI_TRANSFER_NOTHING && + g_object_is_floating (arg->v_pointer)) { + g_object_ref (arg->v_pointer); + object = pygi_marshal_to_py_object (arg, GI_TRANSFER_EVERYTHING); + g_object_force_floating (arg->v_pointer); + } else { + object = pygi_marshal_to_py_object (arg, transfer); + } break; default: diff --git a/gi/pygi-boxed.c b/gi/pygi-boxed.c index ff3db9b..c15c927 100644 --- a/gi/pygi-boxed.c +++ b/gi/pygi-boxed.c @@ -53,24 +53,19 @@ _pygi_boxed_alloc (GIBaseInfo *info, gsize *size_out) { gsize size; - /* FIXME: Remove when bgo#622711 is fixed */ - if (g_registered_type_info_get_g_type (info) == G_TYPE_VALUE) { - size = sizeof (GValue); - } else { - switch (g_base_info_get_type (info)) { - case GI_INFO_TYPE_UNION: - size = g_union_info_get_size ( (GIUnionInfo *) info); - break; - case GI_INFO_TYPE_BOXED: - case GI_INFO_TYPE_STRUCT: - size = g_struct_info_get_size ( (GIStructInfo *) info); - break; - default: - PyErr_Format (PyExc_TypeError, - "info should be Boxed or Union, not '%d'", - g_base_info_get_type (info)); - return NULL; - } + switch (g_base_info_get_type (info)) { + case GI_INFO_TYPE_UNION: + size = g_union_info_get_size ( (GIUnionInfo *) info); + break; + case GI_INFO_TYPE_BOXED: + case GI_INFO_TYPE_STRUCT: + size = g_struct_info_get_size ( (GIStructInfo *) info); + break; + default: + PyErr_Format (PyExc_TypeError, + "info should be Boxed or Union, not '%d'", + g_base_info_get_type (info)); + return NULL; } if( size_out != NULL) diff --git a/gi/pygi-marshal-from-py.c b/gi/pygi-marshal-from-py.c index 9f7d874..01138bc 100644 --- a/gi/pygi-marshal-from-py.c +++ b/gi/pygi-marshal-from-py.c @@ -893,7 +893,7 @@ _pygi_marshal_from_py_array (PyGIInvokeState *state, GIArgument *arg) { PyGIMarshalFromPyFunc from_py_marshaller; - int i; + int i = 0; Py_ssize_t length; gssize item_size; gboolean is_ptr_array; @@ -1629,53 +1629,11 @@ _pygi_marshal_from_py_interface_struct (PyGIInvokeState *state, */ if (iface_cache->g_type == G_TYPE_CLOSURE) { - GClosure *closure; - GType object_gtype = pyg_type_from_object_strict (py_arg, FALSE); - - if ( !(PyCallable_Check(py_arg) || - g_type_is_a (object_gtype, G_TYPE_CLOSURE))) { - PyErr_Format (PyExc_TypeError, "Must be callable, not %s", - py_arg->ob_type->tp_name); - return FALSE; - } - - if (g_type_is_a (object_gtype, G_TYPE_CLOSURE)) - closure = (GClosure *)pyg_boxed_get (py_arg, void); - else - closure = pyg_closure_new (py_arg, NULL, NULL); - - if (closure == NULL) { - PyErr_SetString (PyExc_RuntimeError, "PyObject conversion to GClosure failed"); - return FALSE; - } - - arg->v_pointer = closure; - return TRUE; + return pygi_marshal_from_py_gclosure (py_arg, arg); } else if (iface_cache->g_type == G_TYPE_VALUE) { - GValue *value; - GType object_type; - - object_type = pyg_type_from_object_strict ( (PyObject *) py_arg->ob_type, FALSE); - if (object_type == G_TYPE_INVALID) { - PyErr_SetString (PyExc_RuntimeError, "unable to retrieve object's GType"); - return FALSE; - } - - /* if already a gvalue, use that, else marshal into gvalue */ - if (object_type == G_TYPE_VALUE) { - value = (GValue *)( (PyGObject *)py_arg)->obj; - } else { - value = g_slice_new0 (GValue); - g_value_init (value, object_type); - if (pyg_value_from_pyobject (value, py_arg) < 0) { - g_slice_free (GValue, value); - PyErr_SetString (PyExc_RuntimeError, "PyObject conversion to GValue failed"); - return FALSE; - } - } - - arg->v_pointer = value; - return TRUE; + return pygi_marshal_from_py_gvalue(py_arg, arg, + arg_cache->transfer, + arg_cache->is_caller_allocates); } else if (iface_cache->is_foreign) { PyObject *success; success = pygi_struct_foreign_convert_to_g_argument (py_arg, @@ -1760,11 +1718,7 @@ _pygi_marshal_from_py_interface_object (PyGIInvokeState *state, return FALSE; } - arg->v_pointer = pygobject_get(py_arg); - if (arg_cache->transfer == GI_TRANSFER_EVERYTHING) - g_object_ref (arg->v_pointer); - - return TRUE; + return pygi_marshal_from_py_gobject (py_arg, arg, arg_cache->transfer); } gboolean @@ -1855,3 +1809,141 @@ gboolean _pygi_marshal_from_py_interface_instance (PyGIInvokeState *state, return TRUE; } + +/* pygi_marshal_from_py_gobject: + * py_arg: (in): + * arg: (out): + */ +gboolean +pygi_marshal_from_py_gobject (PyObject *py_arg, /*in*/ + GIArgument *arg, /*out*/ + GITransfer transfer) { + GObject *gobj; + + if (py_arg == Py_None) { + arg->v_pointer = NULL; + return TRUE; + } + + gobj = pygobject_get (py_arg); + if (transfer == GI_TRANSFER_EVERYTHING) { + /* An easy case of adding a new ref that the caller will take ownership of. + * Pythons existing ref to the GObject will be managed normally with the wrapper. + */ + g_object_ref (gobj); + + } else if (py_arg->ob_refcnt == 1 && gobj->ref_count == 1) { + /* If both object ref counts are only 1 at this point (the reference held + * in a return tuple), we assume the GObject will be free'd before reaching + * its target and become invalid. So instead of getting invalid object errors + * we add a new GObject ref. + */ + g_object_ref (gobj); + + if (((PyGObject *)py_arg)->private_flags.flags & PYGOBJECT_GOBJECT_WAS_FLOATING) { + /* HACK: + * We want to re-float instances that were floating and the Python + * wrapper assumed ownership. With the additional caveat that there + * are not any strong references beyond the return tuple. + * This should be removed once the following ticket is fixed: + * https://bugzilla.gnome.org/show_bug.cgi?id=693393 + */ + g_object_force_floating (gobj); + + } else { + PyObject *repr = PyObject_Repr (py_arg); + gchar *msg = g_strdup_printf ("Expecting to marshal a borrowed reference for %s, " + "but nothing in Python is holding a reference to this object. " + "See: https://bugzilla.gnome.org/show_bug.cgi?id=687522", + PYGLIB_PyUnicode_AsString(repr)); + Py_DECREF (repr); + if (PyErr_WarnEx (PyExc_RuntimeWarning, msg, 2)) { + g_free (msg); + return FALSE; + } + g_free (msg); + } + } + + arg->v_pointer = gobj; + return TRUE; +} + +/* pygi_marshal_from_py_gvalue: + * py_arg: (in): + * arg: (out): + * transfer: + * is_allocated: TRUE if arg->v_pointer is an already allocated GValue + */ +gboolean +pygi_marshal_from_py_gvalue (PyObject *py_arg, + GIArgument *arg, + GITransfer transfer, + gboolean is_allocated) { + GValue *value; + GType object_type; + + object_type = pyg_type_from_object_strict ( (PyObject *) py_arg->ob_type, FALSE); + if (object_type == G_TYPE_INVALID) { + PyErr_SetString (PyExc_RuntimeError, "unable to retrieve object's GType"); + return FALSE; + } + + if (is_allocated) + value = (GValue *)arg->v_pointer; + else + value = g_slice_new0 (GValue); + + /* if already a gvalue, use that, else marshal into gvalue */ + if (object_type == G_TYPE_VALUE) { + GValue *source_value = pyg_boxed_get (py_arg, GValue); + if (G_VALUE_TYPE (value) == G_TYPE_INVALID) + g_value_init (value, G_VALUE_TYPE (source_value)); + g_value_copy (source_value, value); + } else { + if (G_VALUE_TYPE (value) == G_TYPE_INVALID) + g_value_init (value, object_type); + + if (pyg_value_from_pyobject (value, py_arg) < 0) { + if (!is_allocated) + g_slice_free (GValue, value); + PyErr_SetString (PyExc_RuntimeError, "PyObject conversion to GValue failed"); + return FALSE; + } + } + + arg->v_pointer = value; + return TRUE; +} + +/* pygi_marshal_from_py_gclosure: + * py_arg: (in): + * arg: (out): + */ +gboolean +pygi_marshal_from_py_gclosure(PyObject *py_arg, + GIArgument *arg) +{ + GClosure *closure; + GType object_gtype = pyg_type_from_object_strict (py_arg, FALSE); + + if ( !(PyCallable_Check(py_arg) || + g_type_is_a (object_gtype, G_TYPE_CLOSURE))) { + PyErr_Format (PyExc_TypeError, "Must be callable, not %s", + py_arg->ob_type->tp_name); + return FALSE; + } + + if (g_type_is_a (object_gtype, G_TYPE_CLOSURE)) + closure = (GClosure *)pyg_boxed_get (py_arg, void); + else + closure = pyg_closure_new (py_arg, NULL, NULL); + + if (closure == NULL) { + PyErr_SetString (PyExc_RuntimeError, "PyObject conversion to GClosure failed"); + return FALSE; + } + + arg->v_pointer = closure; + return TRUE; +} diff --git a/gi/pygi-marshal-from-py.h b/gi/pygi-marshal-from-py.h index b82f890..e0a57d3 100644 --- a/gi/pygi-marshal-from-py.h +++ b/gi/pygi-marshal-from-py.h @@ -184,6 +184,20 @@ gboolean _pygi_marshal_from_py_interface_instance (PyGIInvokeState *state, PyObject *py_arg, GIArgument *arg); +/* Simplified marshalers shared between vfunc/closure and direct function calls. */ + +gboolean pygi_marshal_from_py_gobject (PyObject *py_arg, /*in*/ + GIArgument *arg, /*out*/ + GITransfer transfer); + +gboolean pygi_marshal_from_py_gvalue (PyObject *py_arg, /*in*/ + GIArgument *arg, /*out*/ + GITransfer transfer, + gboolean is_allocated); + +gboolean pygi_marshal_from_py_gclosure(PyObject *py_arg, /*in*/ + GIArgument *arg); /*out*/ + G_END_DECLS #endif /* __PYGI_MARSHAL_from_py_PY__ */ diff --git a/gi/pygi-marshal-to-py.c b/gi/pygi-marshal-to-py.c index a6d90c2..172561d 100644 --- a/gi/pygi-marshal-to-py.c +++ b/gi/pygi-marshal-to-py.c @@ -877,28 +877,7 @@ _pygi_marshal_to_py_interface_object (PyGIInvokeState *state, PyGIArgCache *arg_cache, GIArgument *arg) { - PyObject *py_obj; - - if (arg->v_pointer == NULL) { - py_obj = Py_None; - Py_INCREF (py_obj); - return py_obj; - } - - if (G_IS_PARAM_SPEC(arg->v_pointer)) - { - py_obj = pyg_param_spec_new (arg->v_pointer); - if (arg_cache->transfer == GI_TRANSFER_EVERYTHING) - g_param_spec_unref (arg->v_pointer); - } - else - { - py_obj = pygobject_new (arg->v_pointer); - if (arg_cache->transfer == GI_TRANSFER_EVERYTHING) - g_object_unref (arg->v_pointer); - } - - return py_obj; + return pygi_marshal_to_py_object(arg, arg_cache->transfer); } PyObject * @@ -913,3 +892,25 @@ _pygi_marshal_to_py_interface_union (PyGIInvokeState *state, "Marshalling for this type is not implemented yet"); return py_obj; } + +PyObject * +pygi_marshal_to_py_object (GIArgument *arg, GITransfer transfer) { + PyObject *pyobj; + + if (arg->v_pointer == NULL) { + pyobj = Py_None; + Py_INCREF (pyobj); + + } else if (G_IS_PARAM_SPEC(arg->v_pointer)) { + pyobj = pyg_param_spec_new (arg->v_pointer); + if (transfer == GI_TRANSFER_EVERYTHING) + g_param_spec_unref (arg->v_pointer); + + } else { + pyobj = pygobject_new_full (arg->v_pointer, + /*steal=*/ transfer == GI_TRANSFER_EVERYTHING, + /*type=*/ NULL); + } + + return pyobj; +} diff --git a/gi/pygi-marshal-to-py.h b/gi/pygi-marshal-to-py.h index a07a13e..f2e1a5c 100644 --- a/gi/pygi-marshal-to-py.h +++ b/gi/pygi-marshal-to-py.h @@ -139,6 +139,11 @@ PyObject *_pygi_marshal_to_py_interface_union (PyGIInvokeState *state, PyGIArgCache *arg_cache, GIArgument *arg); +/* Simplified marshalers shared between vfunc/closure and direct function calls. */ + +PyObject *pygi_marshal_to_py_object (GIArgument *arg, + GITransfer transfer); + G_END_DECLS #endif /* __PYGI_MARSHAL_TO_PY_H__ */ diff --git a/gi/pygtkcompat.py b/gi/pygtkcompat.py index 91b5cc1..9a80eef 100644 --- a/gi/pygtkcompat.py +++ b/gi/pygtkcompat.py @@ -2,7 +2,7 @@ import warnings from gi import PyGIDeprecationWarning warnings.warn('gi.pygtkcompat is being deprecated in favor of using "pygtkcompat" directly.', - PyGIDeprecationWarning, stacklevel=2) + PyGIDeprecationWarning) # pyflakes.ignore from pygtkcompat import (enable, diff --git a/gi/types.py b/gi/types.py index f049da7..c44833d 100644 --- a/gi/types.py +++ b/gi/types.py @@ -100,14 +100,14 @@ def wraps_callable_info(info): def update_func(func): func.__info__ = info func.__name__ = info.get_name() - func.__module__ = info.get_namespace() + func.__module__ = 'gi.repository.' + info.get_namespace() func.__doc__ = get_callable_info_doc_string(info) return func return update_func def Function(info): - """Warps GIFunctionInfo""" + """Wraps GIFunctionInfo""" @wraps_callable_info(info) def function(*args, **kwargs): return info.invoke(*args, **kwargs) @@ -128,7 +128,7 @@ class NativeVFunc(object): def Constructor(info): - """Warps GIFunctionInfo with get_constructor() == True""" + """Wraps GIFunctionInfo with get_constructor() == True""" @wraps_callable_info(info) def constructor(cls, *args, **kwargs): cls_name = info.get_container().get_name() diff --git a/pygtkcompat/Makefile.am b/pygtkcompat/Makefile.am index 6a73cb4..914b3e2 100644 --- a/pygtkcompat/Makefile.am +++ b/pygtkcompat/Makefile.am @@ -2,6 +2,7 @@ pygtkcompatdir = $(pyexecdir)/pygtkcompat pygtkcompat_PYTHON = \ __init__.py \ + generictreemodel.py \ pygtkcompat.py # if we build in a separate tree, we need to symlink the *.py files from the diff --git a/pygtkcompat/Makefile.in b/pygtkcompat/Makefile.in index ff8ad79..9fddcf1 100644 --- a/pygtkcompat/Makefile.in +++ b/pygtkcompat/Makefile.in @@ -274,6 +274,7 @@ top_srcdir = @top_srcdir@ pygtkcompatdir = $(pyexecdir)/pygtkcompat pygtkcompat_PYTHON = \ __init__.py \ + generictreemodel.py \ pygtkcompat.py all: all-am diff --git a/pygtkcompat/generictreemodel.py b/pygtkcompat/generictreemodel.py new file mode 100644 index 0000000..ad67d90 --- /dev/null +++ b/pygtkcompat/generictreemodel.py @@ -0,0 +1,420 @@ +# -*- Mode: Python; py-indent-offset: 4 -*- +# generictreemodel - GenericTreeModel implementation for pygtk compatibility. +# Copyright (C) 2013 Simon Feltman +# +# generictreemodel.py: GenericTreeModel implementation for pygtk compatibility +# +# 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 Street, Fifth Floor, Boston, MA 02110-1301 +# USA + + +# System +import sys +import random +import collections +import ctypes + +# GObject +from gi.repository import GObject +from gi.repository import Gtk + + +class _CTreeIter(ctypes.Structure): + _fields_ = [('stamp', ctypes.c_int), + ('user_data', ctypes.c_void_p), + ('user_data2', ctypes.c_void_p), + ('user_data3', ctypes.c_void_p)] + + @classmethod + def from_iter(cls, iter): + offset = sys.getsizeof(object()) # size of PyObject_HEAD + return ctypes.POINTER(cls).from_address(id(iter) + offset) + + +def _get_user_data_as_pyobject(iter): + citer = _CTreeIter.from_iter(iter) + return ctypes.cast(citer.contents.user_data, ctypes.py_object).value + + +def handle_exception(default_return): + """Returns a function which can act as a decorator for wrapping exceptions and + returning "default_return" upon an exception being thrown. + + This is used to wrap Gtk.TreeModel "do_" method implementations so we can return + a proper value from the override upon an exception occurring with client code + implemented by the "on_" methods. + """ + def decorator(func): + def wrapped_func(*args, **kargs): + try: + return func(*args, **kargs) + except: + # Use excepthook directly to avoid any printing to the screen + # if someone installed an except hook. + sys.excepthook(*sys.exc_info()) + return default_return + return wrapped_func + return decorator + + +class GenericTreeModel(GObject.GObject, Gtk.TreeModel): + """A base implementation of a Gtk.TreeModel for python. + + The GenericTreeModel eases implementing the Gtk.TreeModel interface in Python. + The class can be subclassed to provide a TreeModel implementation which works + directly with Python objects instead of iterators. + + All of the on_* methods should be overridden by subclasses to provide the + underlying implementation a way to access custom model data. For the purposes of + this API, all custom model data supplied or handed back through the overridable + API will use the argument names: node, parent, and child in regards to user data + python objects. + + The create_tree_iter, set_user_data, invalidate_iters, iter_is_valid methods are + available to help manage Gtk.TreeIter objects and their Python object references. + + GenericTreeModel manages a pool of user data nodes that have been used with iters. + This pool stores a references to user data nodes as a dictionary value with the + key being the integer id of the data. This id is what the Gtk.TreeIter objects + use to reference data in the pool. + References will be removed from the pool when the model is deleted or explicitly + by using the optional "node" argument to the "row_deleted" method when notifying + the model of row deletion. + """ + + leak_references = GObject.Property(default=True, type=bool, + blurb="If True, strong references to user data attached to iters are " + "stored in a dictionary pool (default). Otherwise the user data is " + "stored as a raw pointer to a python object without a reference.") + + # + # Methods + # + def __init__(self): + """Initialize. Make sure to call this from derived classes if overridden.""" + super(GenericTreeModel, self).__init__() + self.stamp = 0 + + #: Dictionary of (id(user_data): user_data), used when leak-refernces=False + self._held_refs = dict() + + # Set initial stamp + self.invalidate_iters() + + def iter_depth_first(self): + """Depth-first iteration of the entire TreeModel yielding the python nodes.""" + stack = collections.deque([None]) + while stack: + it = stack.popleft() + if it is not None: + yield self.get_user_data(it) + children = [self.iter_nth_child(it, i) for i in range(self.iter_n_children(it))] + stack.extendleft(reversed(children)) + + def invalidate_iter(self, iter): + """Clear user data and its reference from the iter and this model.""" + iter.stamp = 0 + if iter.user_data: + if iter.user_data in self._held_refs: + del self._held_refs[iter.user_data] + iter.user_data = None + + def invalidate_iters(self): + """ + This method invalidates all TreeIter objects associated with this custom tree model + and frees their locally pooled references. + """ + self.stamp = random.randint(-2147483648, 2147483647) + self._held_refs.clear() + + def iter_is_valid(self, iter): + """ + :Returns: + True if the gtk.TreeIter specified by iter is valid for the custom tree model. + """ + return iter.stamp == self.stamp + + def get_user_data(self, iter): + """Get the user_data associated with the given TreeIter. + + GenericTreeModel stores arbitrary Python objects mapped to instances of Gtk.TreeIter. + This method allows to retrieve the Python object held by the given iterator. + """ + if self.leak_references: + return self._held_refs[iter.user_data] + else: + return _get_user_data_as_pyobject(iter) + + def set_user_data(self, iter, user_data): + """Applies user_data and stamp to the given iter. + + If the models "leak_references" property is set, a reference to the + user_data is stored with the model to ensure we don't run into bad + memory problems with the TreeIter. + """ + iter.user_data = id(user_data) + + if user_data is None: + self.invalidate_iter(iter) + else: + iter.stamp = self.stamp + if self.leak_references: + self._held_refs[iter.user_data] = user_data + + def create_tree_iter(self, user_data): + """Create a Gtk.TreeIter instance with the given user_data specific for this model. + + Use this method to create Gtk.TreeIter instance instead of directly calling + Gtk.Treeiter(), this will ensure proper reference managment of wrapped used_data. + """ + iter = Gtk.TreeIter() + self.set_user_data(iter, user_data) + return iter + + def _create_tree_iter(self, data): + """Internal creation of a (bool, TreeIter) pair for returning directly + back to the view interfacing with this model.""" + if data is None: + return (False, None) + else: + it = self.create_tree_iter(data) + return (True, it) + + def row_deleted(self, path, node=None): + """Notify the model a row has been deleted. + + Use the node parameter to ensure the user_data reference associated + with the path is properly freed by this model. + + :Parameters: + path : Gtk.TreePath + Path to the row that has been deleted. + node : object + Python object used as the node returned from "on_get_iter". This is + optional but ensures the model will not leak references to this object. + """ + super(GenericTreeModel, self).row_deleted(path) + node_id = id(node) + if node_id in self._held_refs: + del self._held_refs[node_id] + + # + # GtkTreeModel Interface Implementation + # + @handle_exception(0) + def do_get_flags(self): + """Internal method.""" + return self.on_get_flags() + + @handle_exception(0) + def do_get_n_columns(self): + """Internal method.""" + return self.on_get_n_columns() + + @handle_exception((False, None)) + def do_get_column_type(self, index): + """Internal method.""" + return self.on_get_column_type(index) + + @handle_exception((False, None)) + def do_get_iter(self, path): + """Internal method.""" + return self._create_tree_iter(self.on_get_iter(path)) + + @handle_exception(False) + def do_iter_next(self, iter): + """Internal method.""" + if iter is None: + next_data = self.on_iter_next(None) + else: + next_data = self.on_iter_next(self.get_user_data(iter)) + + self.set_user_data(iter, next_data) + return next_data is not None + + @handle_exception(None) + def do_get_path(self, iter): + """Internal method.""" + path = self.on_get_path(self.get_user_data(iter)) + if path is None: + return None + else: + return Gtk.TreePath(path) + + @handle_exception(None) + def do_get_value(self, iter, column): + """Internal method.""" + return self.on_get_value(self.get_user_data(iter), column) + + @handle_exception((False, None)) + def do_iter_children(self, parent): + """Internal method.""" + data = self.get_user_data(parent) if parent else None + return self._create_tree_iter(self.on_iter_children(data)) + + @handle_exception(False) + def do_iter_has_child(self, parent): + """Internal method.""" + return self.on_iter_has_child(self.get_user_data(parent)) + + @handle_exception(0) + def do_iter_n_children(self, iter): + """Internal method.""" + if iter is None: + return self.on_iter_n_children(None) + return self.on_iter_n_children(self.get_user_data(iter)) + + @handle_exception((False, None)) + def do_iter_nth_child(self, parent, n): + """Internal method.""" + if parent is None: + data = self.on_iter_nth_child(None, n) + else: + data = self.on_iter_nth_child(self.get_user_data(parent), n) + return self._create_tree_iter(data) + + @handle_exception((False, None)) + def do_iter_parent(self, child): + """Internal method.""" + return self._create_tree_iter(self.on_iter_parent(self.get_user_data(child))) + + @handle_exception(None) + def do_ref_node(self, iter): + self.on_ref_node(self.get_user_data(iter)) + + @handle_exception(None) + def do_unref_node(self, iter): + self.on_unref_node(self.get_user_data(iter)) + + # + # Python Subclass Overridables + # + def on_get_flags(self): + """Overridable. + + :Returns Gtk.TreeModelFlags: + The flags for this model. See: Gtk.TreeModelFlags + """ + raise NotImplementedError + + def on_get_n_columns(self): + """Overridable. + + :Returns: + The number of columns for this model. + """ + raise NotImplementedError + + def on_get_column_type(self, index): + """Overridable. + + :Returns: + The column type for the given index. + """ + raise NotImplementedError + + def on_get_iter(self, path): + """Overridable. + + :Returns: + A python object (node) for the given TreePath. + """ + raise NotImplementedError + + def on_iter_next(self, node): + """Overridable. + + :Parameters: + node : object + Node at current level. + + :Returns: + A python object (node) following the given node at the current level. + """ + raise NotImplementedError + + def on_get_path(self, node): + """Overridable. + + :Returns: + A TreePath for the given node. + """ + raise NotImplementedError + + def on_get_value(self, node, column): + """Overridable. + + :Parameters: + node : object + column : int + Column index to get the value from. + + :Returns: + The value of the column for the given node.""" + raise NotImplementedError + + def on_iter_children(self, parent): + """Overridable. + + :Returns: + The first child of parent or None if parent has no children. + If parent is None, return the first node of the model. + """ + raise NotImplementedError + + def on_iter_has_child(self, node): + """Overridable. + + :Returns: + True if the given node has children. + """ + raise NotImplementedError + + def on_iter_n_children(self, node): + """Overridable. + + :Returns: + The number of children for the given node. If node is None, + return the number of top level nodes. + """ + raise NotImplementedError + + def on_iter_nth_child(self, parent, n): + """Overridable. + + :Parameters: + parent : object + n : int + Index of child within parent. + + :Returns: + The child for the given parent index starting at 0. If parent None, + return the top level node corresponding to "n". + If "n" is larger then available nodes, return None. + """ + raise NotImplementedError + + def on_iter_parent(self, child): + """Overridable. + + :Returns: + The parent node of child or None if child is a top level node.""" + raise NotImplementedError + + def on_ref_node(self, node): + pass + + def on_unref_node(self, node): + pass diff --git a/pygtkcompat/pygtkcompat.py b/pygtkcompat/pygtkcompat.py index 18ebd7b..02394f9 100644 --- a/pygtkcompat/pygtkcompat.py +++ b/pygtkcompat/pygtkcompat.py @@ -427,6 +427,9 @@ def enable_gtk(version='2.0'): value = getattr(Gdk, name) setattr(keysyms, target, value) + from . import generictreemodel + Gtk.GenericTreeModel = generictreemodel.GenericTreeModel + def enable_vte(): gi.require_version('Vte', '0.0') diff --git a/tests/Makefile.am b/tests/Makefile.am index 1d40539..287542d 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -92,6 +92,7 @@ EXTRA_DIST = \ test_internal_api.py \ test_iochannel.py \ test_mainloop.py \ + test_object_marshaling.py \ test_option.py \ test_properties.py \ test_signal.py \ @@ -107,6 +108,7 @@ EXTRA_DIST = \ test_overrides_gdk.py \ test_overrides_gtk.py \ test_atoms.py \ + test_generictreemodel.py \ compat_test_pygtk.py \ gi/__init__.py \ gi/overrides/__init__.py \ diff --git a/tests/Makefile.in b/tests/Makefile.in index a29792b..5c8f709 100644 --- a/tests/Makefile.in +++ b/tests/Makefile.in @@ -337,6 +337,7 @@ EXTRA_DIST = \ test_internal_api.py \ test_iochannel.py \ test_mainloop.py \ + test_object_marshaling.py \ test_option.py \ test_properties.py \ test_signal.py \ @@ -352,6 +353,7 @@ EXTRA_DIST = \ test_overrides_gdk.py \ test_overrides_gtk.py \ test_atoms.py \ + test_generictreemodel.py \ compat_test_pygtk.py \ gi/__init__.py \ gi/overrides/__init__.py \ diff --git a/tests/test_generictreemodel.py b/tests/test_generictreemodel.py new file mode 100644 index 0000000..ff0f523 --- /dev/null +++ b/tests/test_generictreemodel.py @@ -0,0 +1,406 @@ +# -*- Mode: Python; py-indent-offset: 4 -*- +# test_generictreemodel - Tests for GenericTreeModel +# Copyright (C) 2013 Simon Feltman +# +# test_generictreemodel.py: Tests for GenericTreeModel +# +# 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 Street, Fifth Floor, Boston, MA 02110-1301 +# USA + + +# system +import gc +import sys +import weakref +import unittest + +# pygobject +from gi.repository import GObject +from gi.repository import Gtk +from pygtkcompat.generictreemodel import GenericTreeModel +from pygtkcompat.generictreemodel import _get_user_data_as_pyobject + + +class Node(object): + """Represents a generic node with name, value, and children.""" + def __init__(self, name, value, *children): + self.name = name + self.value = value + self.children = list(children) + self.parent = None + self.next = None + + for i, child in enumerate(children): + child.parent = weakref.ref(self) + if i < len(children) - 1: + child.next = weakref.ref(children[i + 1]) + + def __repr__(self): + return 'Node("%s", %s)' % (self.name, self.value) + + +class TesterModel(GenericTreeModel): + def __init__(self): + super(TesterModel, self).__init__() + self.root = Node('root', 0, + Node('spam', 1, + Node('sushi', 2), + Node('bread', 3) + ), + Node('eggs', 4) + ) + + def on_get_flags(self): + return 0 + + def on_get_n_columns(self): + return 2 + + def on_get_column_type(self, n): + return (str, int)[n] + + def on_get_iter(self, path): + node = self.root + path = list(path) + idx = path.pop(0) + while path: + idx = path.pop(0) + node = node.children[idx] + return node + + def on_get_path(self, node): + def rec_get_path(n): + for i, child in enumerate(n.children): + if child == node: + return [i] + else: + res = rec_get_path(child) + if res: + res.insert(0, i) + + return rec_get_path(self.root) + + def on_get_value(self, node, column): + if column == 0: + return node.name + elif column == 1: + return node.value + + def on_iter_has_child(self, node): + return bool(node.children) + + def on_iter_next(self, node): + if node.next: + return node.next() + + def on_iter_children(self, node): + if node: + return node.children[0] + else: + return self.root + + def on_iter_n_children(self, node): + if node is None: + return 1 + return len(node.children) + + def on_iter_nth_child(self, node, n): + if node is None: + assert n == 0 + return self.root + return node.children[n] + + def on_iter_parent(self, child): + if child.parent: + return child.parent() + + +class TestReferences(unittest.TestCase): + def setUp(self): + pass + + def test_c_tree_iter_user_data_as_pyobject(self): + obj = object() + obj_id = id(obj) + ref_count = sys.getrefcount(obj) + + # This is essentially a stolen ref in the context of _CTreeIter.get_user_data_as_pyobject + it = Gtk.TreeIter() + it.user_data = obj_id + + obj2 = _get_user_data_as_pyobject(it) + self.assertEqual(obj, obj2) + self.assertEqual(sys.getrefcount(obj), ref_count + 1) + + def test_leak_references_on(self): + model = TesterModel() + obj_ref = weakref.ref(model.root) + # Initial refcount is 1 for model.root + the temporary + self.assertEqual(sys.getrefcount(model.root), 2) + + # Iter increases by 1 do to assignment to iter.user_data + res, it = model.do_get_iter([0]) + self.assertEqual(id(model.root), it.user_data) + self.assertEqual(sys.getrefcount(model.root), 3) + + # Verify getting a TreeIter more then once does not further increase + # the ref count. + res2, it2 = model.do_get_iter([0]) + self.assertEqual(id(model.root), it2.user_data) + self.assertEqual(sys.getrefcount(model.root), 3) + + # Deleting the iter does not decrease refcount because references + # leak by default (they are stored in the held_refs pool) + del it + gc.collect() + self.assertEqual(sys.getrefcount(model.root), 3) + + # Deleting a model should free all held references to user data + # stored by TreeIters + del model + gc.collect() + self.assertEqual(obj_ref(), None) + + def test_row_deleted_frees_refs(self): + model = TesterModel() + obj_ref = weakref.ref(model.root) + # Initial refcount is 1 for model.root + the temporary + self.assertEqual(sys.getrefcount(model.root), 2) + + # Iter increases by 1 do to assignment to iter.user_data + res, it = model.do_get_iter([0]) + self.assertEqual(id(model.root), it.user_data) + self.assertEqual(sys.getrefcount(model.root), 3) + + # Notifying the underlying model of a row_deleted should decrease the + # ref count. + model.row_deleted(Gtk.TreePath('0'), model.root) + self.assertEqual(sys.getrefcount(model.root), 2) + + # Finally deleting the actual object should collect it completely + del model.root + gc.collect() + self.assertEqual(obj_ref(), None) + + def test_leak_references_off(self): + model = TesterModel() + model.leak_references = False + + obj_ref = weakref.ref(model.root) + # Initial refcount is 1 for model.root + the temporary + self.assertEqual(sys.getrefcount(model.root), 2) + + # Iter does not increas count by 1 when leak_references is false + res, it = model.do_get_iter([0]) + self.assertEqual(id(model.root), it.user_data) + self.assertEqual(sys.getrefcount(model.root), 2) + + # Deleting the iter does not decrease refcount because assigning user_data + # eats references and does not release them. + del it + gc.collect() + self.assertEqual(sys.getrefcount(model.root), 2) + + # Deleting the model decreases the final ref, and the object is collected + del model + gc.collect() + self.assertEqual(obj_ref(), None) + + def test_iteration_refs(self): + # Pull iterators off the model using the wrapped C API which will + # then call back into the python overrides. + model = TesterModel() + nodes = [node for node in model.iter_depth_first()] + values = [node.value for node in nodes] + + # Verify depth first ordering + self.assertEqual(values, [0, 1, 2, 3, 4]) + + # Verify ref counts for each of the nodes. + # 5 refs for each node at this point: + # 1 - ref held in getrefcount function + # 2 - ref held by "node" var during iteration + # 3 - ref held by local "nodes" var + # 4 - ref held by the root/children graph itself + # 5 - ref held by the model "held_refs" instance var + for node in nodes: + self.assertEqual(sys.getrefcount(node), 5) + + # A second iteration and storage of the nodes in a new list + # should only increase refcounts by 1 even though new + # iterators are created and assigned. + nodes2 = [node for node in model.iter_depth_first()] + for node in nodes2: + self.assertEqual(sys.getrefcount(node), 6) + + # Hold weak refs and start verifying ref collection. + node_refs = [weakref.ref(node) for node in nodes] + + # First round of collection + del nodes2 + gc.collect() + for node in nodes: + self.assertEqual(sys.getrefcount(node), 5) + + # Second round of collection, no more local lists of nodes. + del nodes + gc.collect() + for ref in node_refs: + node = ref() + self.assertEqual(sys.getrefcount(node), 4) + + # Using invalidate_iters or row_deleted(path, node) will clear out + # the pooled refs held internal to the GenericTreeModel implementation. + model.invalidate_iters() + self.assertEqual(len(model._held_refs), 0) + gc.collect() + for ref in node_refs: + node = ref() + self.assertEqual(sys.getrefcount(node), 3) + + # Deleting the root node at this point should allow all nodes to be collected + # as there is no longer a way to reach the children + del node # node still in locals() from last iteration + del model.root + gc.collect() + for ref in node_refs: + self.assertEqual(ref(), None) + + +class TestIteration(unittest.TestCase): + def test_iter_next_root(self): + model = TesterModel() + it = model.get_iter([0]) + self.assertEqual(it.user_data, id(model.root)) + self.assertEqual(model.root.next, None) + + it = model.iter_next(it) + self.assertEqual(it, None) + + def test_iter_next_multiple(self): + model = TesterModel() + it = model.get_iter([0, 0]) + self.assertEqual(it.user_data, id(model.root.children[0])) + + it = model.iter_next(it) + self.assertEqual(it.user_data, id(model.root.children[1])) + + it = model.iter_next(it) + self.assertEqual(it, None) + + +class ErrorModel(GenericTreeModel): + # All on_* methods will raise a NotImplementedError by default + pass + + +class ExceptHook(object): + """ + Temporarily installs an exception hook in a context which + expects the given exc_type to be raised. This allows verification + of exceptions that occur within python gi callbacks but + are never bubbled through from python to C back to python. + This works because exception hooks are called in PyErr_Print. + """ + def __init__(self, exc_type): + self._exc_type = exc_type + self._exceptions = [] + + def _excepthook(self, exc_type, value, traceback): + self._exceptions.append(exc_type) + + def __enter__(self): + self._oldhook = sys.excepthook + sys.excepthook = self._excepthook + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + sys.excepthook = self._oldhook + assert len(self._exceptions) == 1, 'Expecting exactly one exception of type %s' % self._exc_type + assert issubclass(self._exceptions[0], self._exc_type), 'Expecting exactly one exception of type %s' % self._exc_type + + +class TestReturnsAfterError(unittest.TestCase): + def setUp(self): + self.model = ErrorModel() + + def test_get_flags(self): + with ExceptHook(NotImplementedError): + flags = self.model.get_flags() + self.assertEqual(flags, 0) + + def test_get_n_columns(self): + with ExceptHook(NotImplementedError): + count = self.model.get_n_columns() + self.assertEqual(count, 0) + + def test_get_column_type(self): + with ExceptHook(NotImplementedError): + col_type = self.model.get_column_type(0) + self.assertEqual(col_type, GObject.TYPE_INVALID) + + def test_get_iter(self): + with ExceptHook(NotImplementedError): + self.assertRaises(ValueError, self.model.get_iter, Gtk.TreePath(0)) + + def test_get_path(self): + it = self.model.create_tree_iter('foo') + with ExceptHook(NotImplementedError): + path = self.model.get_path(it) + self.assertEqual(path, None) + + def test_get_value(self): + it = self.model.create_tree_iter('foo') + with ExceptHook(NotImplementedError): + try: + self.model.get_value(it, 0) + except TypeError: + pass # silence TypeError converting None to GValue + + def test_iter_has_child(self): + it = self.model.create_tree_iter('foo') + with ExceptHook(NotImplementedError): + res = self.model.iter_has_child(it) + self.assertEqual(res, False) + + def test_iter_next(self): + it = self.model.create_tree_iter('foo') + with ExceptHook(NotImplementedError): + res = self.model.iter_next(it) + self.assertEqual(res, None) + + def test_iter_children(self): + with ExceptHook(NotImplementedError): + res = self.model.iter_children(None) + self.assertEqual(res, None) + + def test_iter_n_children(self): + with ExceptHook(NotImplementedError): + res = self.model.iter_n_children(None) + self.assertEqual(res, 0) + + def test_iter_nth_child(self): + with ExceptHook(NotImplementedError): + res = self.model.iter_nth_child(None, 0) + self.assertEqual(res, None) + + def test_iter_parent(self): + child = self.model.create_tree_iter('foo') + with ExceptHook(NotImplementedError): + res = self.model.iter_parent(child) + self.assertEqual(res, None) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_gi.py b/tests/test_gi.py index ee2f3de..4545539 100644 --- a/tests/test_gi.py +++ b/tests/test_gi.py @@ -2785,6 +2785,12 @@ class TestPropertiesObject(unittest.TestCase): self.assertEqual(obj.props.some_variant.get_type_string(), 'b') self.assertEqual(obj.props.some_variant.get_boolean(), True) + def test_setting_several_properties(self): + obj = GIMarshallingTests.PropertiesObject() + obj.set_properties(some_uchar=54, some_int=42) + self.assertEqual(42, obj.props.some_int) + self.assertEqual(54, obj.props.some_uchar) + class TestKeywords(unittest.TestCase): def test_method(self): diff --git a/tests/test_object_marshaling.py b/tests/test_object_marshaling.py new file mode 100644 index 0000000..62570bc --- /dev/null +++ b/tests/test_object_marshaling.py @@ -0,0 +1,616 @@ +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab + +import unittest +import weakref +import gc +import sys +import warnings + +from gi.repository import GObject +from gi.repository import GIMarshallingTests + + +class StrongRef(object): + # A class that behaves like weakref.ref but holds a strong reference. + # This allows re-use of the VFuncsBase by swapping out the ObjectRef + # class var with either weakref.ref or StrongRef. + + def __init__(self, obj): + self.obj = obj + + def __call__(self): + return self.obj + + +class VFuncsBase(GIMarshallingTests.Object): + # Class which generically implements the vfuncs used for reference counting tests + # in a way that can be easily sub-classed and modified. + + #: Object type used by this class for testing + #: This can be GObject.Object or GObject.InitiallyUnowned + Object = GObject.Object + + #: Reference type used by this class for holding refs to in/out objects. + #: This can be set to weakref.ref or StrongRef + ObjectRef = weakref.ref + + def __init__(self): + super(VFuncsBase, self).__init__() + + #: Hold ref of input or output python wrappers + self.object_ref = None + + #: store grefcount of input object + self.in_object_grefcount = None + self.in_object_is_floating = None + + def do_vfunc_return_object_transfer_none(self): + # Return an object but keep a python reference to it. + obj = self.Object() + self.object_ref = self.ObjectRef(obj) + return obj + + def do_vfunc_return_object_transfer_full(self): + # Return an object and hand off the reference to the caller. + obj = self.Object() + self.object_ref = self.ObjectRef(obj) + return obj + + def do_vfunc_out_object_transfer_none(self): + # Same as do_vfunc_return_object_transfer_none but the pygi + # internals convert the return here into an out arg. + obj = self.Object() + self.object_ref = self.ObjectRef(obj) + return obj + + def do_vfunc_out_object_transfer_full(self): + # Same as do_vfunc_return_object_transfer_full but the pygi + # internals convert the return here into an out arg. + obj = self.Object() + self.object_ref = self.ObjectRef(obj) + return obj + + def do_vfunc_in_object_transfer_none(self, obj): + # 'obj' will have a python wrapper as well as still held + # by the caller. + self.object_ref = self.ObjectRef(obj) + self.in_object_grefcount = obj.__grefcount__ + self.in_object_is_floating = obj.is_floating() + + def do_vfunc_in_object_transfer_full(self, obj): + # 'obj' will now be owned by the Python GObject wrapper. + # When obj goes out of scope and is collected, the GObject + # should also be fully released. + self.object_ref = self.ObjectRef(obj) + self.in_object_grefcount = obj.__grefcount__ + self.in_object_is_floating = obj.is_floating() + + +@unittest.skipUnless(hasattr(VFuncsBase, 'get_ref_info_for_vfunc_return_object_transfer_none') and + hasattr(VFuncsBase, 'get_ref_info_for_vfunc_out_object_transfer_none'), + 'too old gobject-introspection') +class TestVFuncsWithObjectArg(unittest.TestCase): + # Basic set of tests which work on non-floating objects which python does + # not keep an additional reference of. + + class VFuncs(VFuncsBase): + # Object for testing non-floating objects without holding any refs. + Object = GObject.Object + ObjectRef = weakref.ref + + def test_vfunc_self_arg_ref_count(self): + # Check to make sure vfunc "self" arguments don't leak. + vfuncs = self.VFuncs() + vfuncs_ref = weakref.ref(vfuncs) + vfuncs.get_ref_info_for_vfunc_return_object_transfer_full() # Use any vfunc to test this. + + gc.collect() + self.assertEqual(sys.getrefcount(vfuncs), 2) + self.assertEqual(vfuncs.__grefcount__, 1) + + del vfuncs + gc.collect() + self.assertTrue(vfuncs_ref() is None) + + def test_vfunc_return_object_transfer_none(self): + # This tests a problem case where the vfunc returns a GObject owned solely by Python + # but the argument is marked as transfer-none. + # In this case pygobject marshaling adds an additional ref and gives a warning + # of a potential leak. If this occures it is really a bug in the underlying library + # but pygobject tries to react to this in a reasonable way. + vfuncs = self.VFuncs() + with warnings.catch_warnings(record=True) as warn: + warnings.simplefilter('always') + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_out_object_transfer_none() + self.assertTrue(issubclass(warn[0].category, RuntimeWarning)) + + # The ref count of the GObject returned to the caller (get_ref_info_for_vfunc_return_object_transfer_none) + # should be a single floating ref + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + gc.collect() + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_out_object_transfer_none(self): + # Same as above except uses out arg instead of return + vfuncs = self.VFuncs() + with warnings.catch_warnings(record=True) as warn: + warnings.simplefilter('always') + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_out_object_transfer_none() + self.assertTrue(issubclass(warn[0].category, RuntimeWarning)) + + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + gc.collect() + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_return_object_transfer_full(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_return_object_transfer_full() + + # The vfunc caller receives full ownership of a single ref which should not + # be floating. + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + gc.collect() + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_out_object_transfer_full(self): + # Same as above except uses out arg instead of return + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_out_object_transfer_full() + + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + gc.collect() + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_in_object_transfer_none(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_in_object_transfer_none(self.VFuncs.Object) + + gc.collect() + self.assertEqual(vfuncs.in_object_grefcount, 2) # initial + python wrapper + self.assertFalse(vfuncs.in_object_is_floating) + + self.assertEqual(ref_count, 1) # ensure python wrapper released + self.assertFalse(is_floating) + + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_in_object_transfer_full(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_in_object_transfer_full(self.VFuncs.Object) + + gc.collect() + + # python wrapper should take sole ownership of the gobject + self.assertEqual(vfuncs.in_object_grefcount, 1) + self.assertFalse(vfuncs.in_object_is_floating) + + # ensure python wrapper took ownership and released, after vfunc was complete + self.assertEqual(ref_count, 0) + self.assertFalse(is_floating) + + self.assertTrue(vfuncs.object_ref() is None) + + +@unittest.skipUnless(hasattr(VFuncsBase, 'get_ref_info_for_vfunc_return_object_transfer_none') and + hasattr(VFuncsBase, 'get_ref_info_for_vfunc_out_object_transfer_none'), + 'too old gobject-introspection') +class TestVFuncsWithFloatingArg(unittest.TestCase): + # All tests here work with a floating object by using InitiallyUnowned as the argument + + class VFuncs(VFuncsBase): + # Object for testing non-floating objects without holding any refs. + Object = GObject.InitiallyUnowned + ObjectRef = weakref.ref + + def test_vfunc_return_object_transfer_none_with_floating(self): + # Python is expected to return a single floating reference without warning. + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_return_object_transfer_none() + + # The ref count of the GObject returned to the caller (get_ref_info_for_vfunc_return_object_transfer_none) + # should be a single floating ref + self.assertEqual(ref_count, 1) + self.assertTrue(is_floating) + + gc.collect() + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_out_object_transfer_none_with_floating(self): + # Same as above except uses out arg instead of return + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_out_object_transfer_none() + + self.assertEqual(ref_count, 1) + self.assertTrue(is_floating) + + gc.collect() + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_return_object_transfer_full_with_floating(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_return_object_transfer_full() + + # The vfunc caller receives full ownership of a single ref. + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + gc.collect() + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_out_object_transfer_full_with_floating(self): + # Same as above except uses out arg instead of return + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_out_object_transfer_full() + + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + gc.collect() + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_in_object_transfer_none_with_floating(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_in_object_transfer_none(self.VFuncs.Object) + + gc.collect() + + # python wrapper should maintain the object as floating and add an additional ref + self.assertEqual(vfuncs.in_object_grefcount, 2) + self.assertTrue(vfuncs.in_object_is_floating) + + # vfunc caller should only have a single floating ref after the vfunc finishes + self.assertEqual(ref_count, 1) + self.assertTrue(is_floating) + + self.assertTrue(vfuncs.object_ref() is None) + + def test_vfunc_in_object_transfer_full_with_floating(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_in_object_transfer_full(self.VFuncs.Object) + + gc.collect() + + # python wrapper sinks and owns the gobject + self.assertEqual(vfuncs.in_object_grefcount, 1) + self.assertFalse(vfuncs.in_object_is_floating) + + # ensure python wrapper took ownership and released + self.assertEqual(ref_count, 0) + self.assertFalse(is_floating) + + self.assertTrue(vfuncs.object_ref() is None) + + +@unittest.skipUnless(hasattr(VFuncsBase, 'get_ref_info_for_vfunc_return_object_transfer_none') and + hasattr(VFuncsBase, 'get_ref_info_for_vfunc_out_object_transfer_none'), + 'too old gobject-introspection') +class TestVFuncsWithHeldObjectArg(unittest.TestCase): + # Same tests as TestVFuncsWithObjectArg except we hold + # onto the python object reference in all cases. + + class VFuncs(VFuncsBase): + # Object for testing non-floating objects with a held ref. + Object = GObject.Object + ObjectRef = StrongRef + + def test_vfunc_return_object_transfer_none_with_held_object(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_return_object_transfer_none() + + # Python holds the single gobject ref in 'vfuncs.object_ref' + # Because of this, we do not expect a floating ref or a ref increase. + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + # The actual grefcount should stay at 1 even after the vfunc return. + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + self.assertFalse(vfuncs.in_object_is_floating) + + held_object_ref = weakref.ref(vfuncs.object_ref) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_out_object_transfer_none_with_held_object(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_out_object_transfer_none() + + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + self.assertFalse(vfuncs.in_object_is_floating) + + held_object_ref = weakref.ref(vfuncs.object_ref) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_return_object_transfer_full_with_held_object(self): + # The vfunc caller receives full ownership which should not + # be floating. However, the held python wrapper also has a ref. + + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_return_object_transfer_full() + + # Ref count from the perspective of C after the vfunc is called + # The vfunc caller receives a new reference which should not + # be floating. However, the held python wrapper also has a ref. + self.assertEqual(ref_count, 2) + self.assertFalse(is_floating) + + # Current ref count + # The vfunc caller should have decremented its reference. + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_out_object_transfer_full_with_held_object(self): + # Same test as above except uses out arg instead of return + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_out_object_transfer_full() + + # Ref count from the perspective of C after the vfunc is called + # The vfunc caller receives a new reference which should not + # be floating. However, the held python wrapper also has a ref. + self.assertEqual(ref_count, 2) + self.assertFalse(is_floating) + + # Current ref count + # The vfunc caller should have decremented its reference. + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref()) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_in_object_transfer_none_with_held_object(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_in_object_transfer_none(self.VFuncs.Object) + + gc.collect() + + # Ref count inside vfunc from the perspective of Python + self.assertEqual(vfuncs.in_object_grefcount, 2) # initial + python wrapper + self.assertFalse(vfuncs.in_object_is_floating) + + # Ref count from the perspective of C after the vfunc is called + self.assertEqual(ref_count, 2) # kept after vfunc + held python wrapper + self.assertFalse(is_floating) + + # Current ref count after C cleans up its reference + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref()) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_in_object_transfer_full_with_held_object(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_in_object_transfer_full(self.VFuncs.Object) + + gc.collect() + + # Ref count inside vfunc from the perspective of Python + self.assertEqual(vfuncs.in_object_grefcount, 1) # python wrapper takes ownership of the gobject + self.assertFalse(vfuncs.in_object_is_floating) + + # Ref count from the perspective of C after the vfunc is called + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + # Current ref count + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref()) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + +@unittest.skipUnless(hasattr(VFuncsBase, 'get_ref_info_for_vfunc_return_object_transfer_none') and + hasattr(VFuncsBase, 'get_ref_info_for_vfunc_out_object_transfer_none'), + 'too old gobject-introspection') +class TestVFuncsWithHeldFloatingArg(unittest.TestCase): + # Tests for a floating object which we hold a reference to the python wrapper + # on the VFuncs test class. + + class VFuncs(VFuncsBase): + # Object for testing floating objects with a held ref. + Object = GObject.InitiallyUnowned + ObjectRef = StrongRef + + def test_vfunc_return_object_transfer_none_with_held_floating(self): + # Python holds onto the wrapper which basically means the floating ref + # should also be owned by python. + + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_return_object_transfer_none() + + # This is a borrowed ref from what is held in python. + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + # The actual grefcount should stay at 1 even after the vfunc return. + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_out_object_transfer_none_with_held_floating(self): + # Same as above + + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_out_object_transfer_none() + + self.assertEqual(ref_count, 1) + self.assertFalse(is_floating) + + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_return_object_transfer_full_with_held_floating(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_return_object_transfer_full() + + # Ref count from the perspective of C after the vfunc is called + self.assertEqual(ref_count, 2) + self.assertFalse(is_floating) + + # Current ref count + # vfunc wrapper destroyes ref it was given + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_out_object_transfer_full_with_held_floating(self): + # Same test as above except uses out arg instead of return + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_out_object_transfer_full() + + # Ref count from the perspective of C after the vfunc is called + self.assertEqual(ref_count, 2) + self.assertFalse(is_floating) + + # Current ref count + # vfunc wrapper destroyes ref it was given + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref()) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_in_floating_transfer_none_with_held_floating(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_in_object_transfer_none(self.VFuncs.Object) + gc.collect() + + # Ref count inside vfunc from the perspective of Python + self.assertTrue(vfuncs.in_object_is_floating) + self.assertEqual(vfuncs.in_object_grefcount, 2) # python wrapper sinks and owns the gobject + + # Ref count from the perspective of C after the vfunc is called + self.assertTrue(is_floating) + self.assertEqual(ref_count, 2) # floating + held by wrapper + + # Current ref count after C cleans up its reference + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref()) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + def test_vfunc_in_floating_transfer_full_with_held_floating(self): + vfuncs = self.VFuncs() + ref_count, is_floating = vfuncs.get_ref_info_for_vfunc_in_object_transfer_full(self.VFuncs.Object) + gc.collect() + + # Ref count from the perspective of C after the vfunc is called + self.assertEqual(vfuncs.in_object_grefcount, 1) # python wrapper sinks and owns the gobject + self.assertFalse(vfuncs.in_object_is_floating) + + # Ref count from the perspective of C after the vfunc is called + self.assertEqual(ref_count, 1) # held by wrapper + self.assertFalse(is_floating) + + # Current ref count + self.assertEqual(vfuncs.object_ref().__grefcount__, 1) + + held_object_ref = weakref.ref(vfuncs.object_ref()) + del vfuncs.object_ref + gc.collect() + self.assertTrue(held_object_ref() is None) + + +class TestPropertyHoldingObject(unittest.TestCase): + @unittest.expectedFailure # https://bugzilla.gnome.org/show_bug.cgi?id=675726 + def test_props_getter_holding_object_ref_count(self): + holder = GIMarshallingTests.PropertiesObject() + held = GObject.Object() + + self.assertEqual(holder.__grefcount__, 1) + self.assertEqual(held.__grefcount__, 1) + + holder.set_property('some-object', held) + self.assertEqual(holder.__grefcount__, 1) + + initial_ref_count = held.__grefcount__ + holder.props.some_object + gc.collect() + self.assertEqual(held.__grefcount__, initial_ref_count) + + @unittest.skipUnless(hasattr(GIMarshallingTests.PropertiesObject.props, 'some_object'), + 'too old gobject-introspection') + def test_get_property_holding_object_ref_count(self): + holder = GIMarshallingTests.PropertiesObject() + held = GObject.Object() + + self.assertEqual(holder.__grefcount__, 1) + self.assertEqual(held.__grefcount__, 1) + + holder.set_property('some-object', held) + self.assertEqual(holder.__grefcount__, 1) + + initial_ref_count = held.__grefcount__ + holder.get_property('some-object') + gc.collect() + self.assertEqual(held.__grefcount__, initial_ref_count) + + @unittest.expectedFailure # https://bugzilla.gnome.org/show_bug.cgi?id=675726 + def test_props_setter_holding_object_ref_count(self): + holder = GIMarshallingTests.PropertiesObject() + held = GObject.Object() + + self.assertEqual(holder.__grefcount__, 1) + self.assertEqual(held.__grefcount__, 1) + + # Setting property should only increase ref count by 1 + holder.props.some_object = held + self.assertEqual(holder.__grefcount__, 1) + self.assertEqual(held.__grefcount__, 2) + + # Clearing should pull it back down + holder.props.some_object = None + self.assertEqual(held.__grefcount__, 1) + + @unittest.expectedFailure # https://bugzilla.gnome.org/show_bug.cgi?id=675726 + def test_set_property_holding_object_ref_count(self): + holder = GIMarshallingTests.PropertiesObject() + held = GObject.Object() + + self.assertEqual(holder.__grefcount__, 1) + self.assertEqual(held.__grefcount__, 1) + + # Setting property should only increase ref count by 1 + holder.set_property('some-object', held) + self.assertEqual(holder.__grefcount__, 1) + self.assertEqual(held.__grefcount__, 2) + + # Clearing should pull it back down + holder.set_property('some-object', None) + self.assertEqual(held.__grefcount__, 1) diff --git a/tests/test_overrides_gtk.py b/tests/test_overrides_gtk.py index 9315019..69f0d38 100644 --- a/tests/test_overrides_gtk.py +++ b/tests/test_overrides_gtk.py @@ -2,6 +2,7 @@ # coding: UTF-8 # vim: tabstop=4 shiftwidth=4 expandtab +import contextlib import unittest from compathelper import _unicode, _bytes @@ -17,6 +18,38 @@ except ImportError: Gtk = None +@contextlib.contextmanager +def realized(widget): + """Makes sure the widget is realized. + + view = Gtk.TreeView() + with realized(view): + do_something(view) + """ + + if isinstance(widget, Gtk.Window): + toplevel = widget + else: + toplevel = widget.get_parent_window() + + if toplevel is None: + window = Gtk.Window() + window.add(widget) + + widget.realize() + while Gtk.events_pending(): + Gtk.main_iteration() + assert widget.get_realized() + yield widget + + if toplevel is None: + window.remove(widget) + window.destroy() + + while Gtk.events_pending(): + Gtk.main_iteration() + + @unittest.skipUnless(Gtk, 'Gtk not available') class TestGtk(unittest.TestCase): def test_container(self): @@ -519,6 +552,38 @@ class TestGtk(unittest.TestCase): self.assertTrue(hasattr(widget.drag_dest_set_proxy, '__call__')) self.assertTrue(hasattr(widget.drag_get_data, '__call__')) + def test_drag_target_list(self): + mixed_target_list = [Gtk.TargetEntry.new('test0', 0, 0), + ('test1', 1, 1), + Gtk.TargetEntry.new('test2', 2, 2), + ('test3', 3, 3)] + + def _test_target_list(targets): + for i, target in enumerate(targets): + self.assertTrue(isinstance(target, Gtk.TargetEntry)) + self.assertEqual(target.target, 'test' + str(i)) + self.assertEqual(target.flags, i) + self.assertEqual(target.info, i) + + _test_target_list(Gtk._construct_target_list(mixed_target_list)) + + widget = Gtk.Button() + widget.drag_dest_set(Gtk.DestDefaults.DROP, None, Gdk.DragAction.COPY) + widget.drag_dest_set_target_list(mixed_target_list) + widget.drag_dest_get_target_list() + + widget.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, None, Gdk.DragAction.MOVE) + widget.drag_source_set_target_list(mixed_target_list) + widget.drag_source_get_target_list() + + treeview = Gtk.TreeView() + treeview.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, + mixed_target_list, + Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE) + + treeview.enable_model_drag_dest(mixed_target_list, + Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE) + def test_scrollbar(self): # PyGTK compat adjustment = Gtk.Adjustment() @@ -1363,19 +1428,13 @@ class TestTreeView(unittest.TestCase): store.append((0, "foo")) store.append((1, "bar")) view = Gtk.TreeView() - # FIXME: We can't easily call get_cursor() to make sure this works as - # expected as we need to realize and focus the column; the following - # will raise a Gtk-CRITICAL which we ignore for now - old_mask = GLib.log_set_always_fatal( - GLib.LogLevelFlags.LEVEL_WARNING | GLib.LogLevelFlags.LEVEL_ERROR) - try: + + with realized(view): view.set_cursor(store[1].path) view.set_cursor(str(store[1].path)) view.get_cell_area(store[1].path) view.get_cell_area(str(store[1].path)) - finally: - GLib.log_set_always_fatal(old_mask) def test_tree_view_column(self): cell = Gtk.CellRendererText() @@ -1403,28 +1462,21 @@ class TestTreeView(unittest.TestCase): # unconnected tree.insert_column_with_attributes(-1, 'Head4', cell4) - # might cause a Pango warning, do not break on this - old_mask = GLib.log_set_always_fatal( - GLib.LogLevelFlags.LEVEL_CRITICAL | GLib.LogLevelFlags.LEVEL_ERROR) - try: - # We must realize the TreeView for cell.props.text to receive a value - dialog = Gtk.Dialog() - dialog.get_action_area().add(tree) - dialog.show_all() - dialog.hide() - finally: - GLib.log_set_always_fatal(old_mask) + with realized(tree): + tree.set_cursor(model[0].path) + while Gtk.events_pending(): + Gtk.main_iteration() - self.assertEqual(tree.get_column(0).get_title(), 'Head1') - self.assertEqual(tree.get_column(1).get_title(), 'Head2') - self.assertEqual(tree.get_column(2).get_title(), 'Head3') - self.assertEqual(tree.get_column(3).get_title(), 'Head4') + self.assertEqual(tree.get_column(0).get_title(), 'Head1') + self.assertEqual(tree.get_column(1).get_title(), 'Head2') + self.assertEqual(tree.get_column(2).get_title(), 'Head3') + self.assertEqual(tree.get_column(3).get_title(), 'Head4') - # cursor should be at the first row - self.assertEqual(cell1.props.text, 'cell11') - self.assertEqual(cell2.props.text, 'cell12') - self.assertEqual(cell3.props.text, 'cell13') - self.assertEqual(cell4.props.text, None) + # cursor should be at the first row + self.assertEqual(cell1.props.text, 'cell11') + self.assertEqual(cell2.props.text, 'cell12') + self.assertEqual(cell3.props.text, 'cell13') + self.assertEqual(cell4.props.text, None) def test_tree_view_column_set_attributes(self): store = Gtk.ListStore(int, str) @@ -1442,19 +1494,8 @@ class TestTreeView(unittest.TestCase): column.pack_start(cell, expand=True) column.set_attributes(cell, text=1) - # might cause a Pango warning, do not break on this - old_mask = GLib.log_set_always_fatal( - GLib.LogLevelFlags.LEVEL_CRITICAL | GLib.LogLevelFlags.LEVEL_ERROR) - try: - # We must realize the TreeView for cell.props.text to receive a value - dialog = Gtk.Dialog() - dialog.get_action_area().add(treeview) - dialog.show_all() - dialog.hide() - finally: - GLib.log_set_always_fatal(old_mask) - - self.assertTrue(cell.props.text in directors) + with realized(treeview): + self.assertTrue(cell.props.text in directors) def test_tree_selection(self): store = Gtk.ListStore(int, str) |