From 643f146ad057fff9eb8861019db1bba6a20360ea Mon Sep 17 00:00:00 2001 From: OceanWolf Date: Tue, 28 Jul 2015 18:52:20 +0200 Subject: [PATCH 1/2] Fallback to pgi FIX: Old Ubuntu doesn't have libgirepository as a dependancy of gir1.2-gtk-3.0 --- .travis.yml | 7 ++- INSTALL | 4 ++ doc/glossary/index.rst | 11 ++++ doc/users/whats_new/2015-07-30_pgi.rst | 9 +++ lib/matplotlib/__init__.py | 1 + lib/matplotlib/backends/backend_cairo.py | 20 +------ lib/matplotlib/backends/backend_gtk3.py | 33 +++-------- lib/matplotlib/backends/backend_gtk3agg.py | 2 +- lib/matplotlib/backends/backend_gtk3cairo.py | 2 +- lib/matplotlib/backends/cairo_compat.py | 20 +++++++ lib/matplotlib/backends/gtk3_compat.py | 34 +++++++++++ lib/matplotlib/rcsetup.py | 4 ++ lib/matplotlib/tests/test_backend_gtk3.py | 60 ++++++++++++++++++++ 13 files changed, 159 insertions(+), 48 deletions(-) create mode 100644 doc/users/whats_new/2015-07-30_pgi.rst create mode 100644 lib/matplotlib/backends/cairo_compat.py create mode 100644 lib/matplotlib/backends/gtk3_compat.py create mode 100644 lib/matplotlib/tests/test_backend_gtk3.py diff --git a/.travis.yml b/.travis.yml index 9342444f8fca..3ad28f824cb2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,6 +27,9 @@ addons: - graphviz - libgeos-dev - otf-freefont + - libpangocairo-1.0-0 + - libgirepository-1.0-1 + - gir1.2-gtk-3.0 # - fonts-humor-sans # sources: # - debian-sid @@ -112,7 +115,7 @@ install: pip install --upgrade setuptools - | # Install dependencies from pypi - pip install $PRE python-dateutil $NUMPY pyparsing!=2.1.6 $PANDAS pep8 cycler coveralls coverage + pip install $PRE python-dateutil $NUMPY pyparsing!=2.1.6 $PANDAS pep8 cycler coveralls coverage pgi cairocffi pip install $PRE -r doc-requirements.txt # Install nose from a build which has partial @@ -160,7 +163,7 @@ script: if [[ $TRAVIS_OS_NAME == 'osx' ]]; then python tests.py $NOSE_ARGS $TEST_ARGS else - gdb -return-child-result -batch -ex r -ex bt --args python $PYTHON_ARGS tests.py $NOSE_ARGS $TEST_ARGS + xvfb-run gdb -return-child-result -batch -ex r -ex bt --args python $PYTHON_ARGS tests.py $NOSE_ARGS $TEST_ARGS fi else echo The following args are passed to pytest $PYTEST_ARGS diff --git a/INSTALL b/INSTALL index aade12aab122..0fabeb3c5ccf 100644 --- a/INSTALL +++ b/INSTALL @@ -235,6 +235,10 @@ backends and the capabilities they provide. :term:`pyqt` 4.4 or later The Qt4 widgets library python wrappers for the Qt4Agg backend +:term:`PyGObject` or `pgi` + For Gtk3, MPL requires the installation of a GObject introspection library + for python, either `PyGObject` (also known as gi) or `pgi`. + :term:`pygtk` 2.4 or later The python wrappers for the GTK widgets library for use with the GTK or GTKAgg backend diff --git a/doc/glossary/index.rst b/doc/glossary/index.rst index 5f0b683f14cf..ac77926b4952 100644 --- a/doc/glossary/index.rst +++ b/doc/glossary/index.rst @@ -64,6 +64,17 @@ Glossary channel. PDF was designed in part as a next-generation document format to replace postscript + pgi + `pgi ` exists as a relatively new + python wrapper to GTK3 and acts as a pure python alternative to PyGObject. + pgi still exists in its infancy, currently missing many features of + PyGObject. However matplotlib does not use any of these missing features. + + PyGObject + Like :term:`pygtk`, `PyGObject ` provides + python wrappers for the :term:`GTK` widgets library; unlike pygtk, + PyGObject wraps GTK3 instead of the now obsolete GTK2. + pygtk `pygtk `_ provides python wrappers for the :term:`GTK` widgets library for use with the GTK or GTKAgg diff --git a/doc/users/whats_new/2015-07-30_pgi.rst b/doc/users/whats_new/2015-07-30_pgi.rst new file mode 100644 index 000000000000..08d303d84280 --- /dev/null +++ b/doc/users/whats_new/2015-07-30_pgi.rst @@ -0,0 +1,9 @@ +PGI - Pure Python GObject Introspection Bindings +------------------------------------------------ + +For the GTK3 backend, matplotlib now supports PGI bindings as an alternative +to PyGObject. By default matplotlib will still use PyGObject, otherwise it +will look for pgi. You can change this behaviour through the rcParam +backend.gi_preference which takes either a string, or a list of strings in +order of preference. + diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 16969e5545dd..a52616331a51 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1496,6 +1496,7 @@ def _jupyter_nbextension_paths(): 'matplotlib.tests.test_backend_ps', 'matplotlib.tests.test_backend_qt4', 'matplotlib.tests.test_backend_qt5', + 'matplotlib.tests.test_backend_gtk3', 'matplotlib.tests.test_backend_svg', 'matplotlib.tests.test_basic', 'matplotlib.tests.test_bbox_tight', diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index c567a5a2c34c..3e5ef9611cd4 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -29,25 +29,7 @@ def _fn_name(): return sys._getframe(1).f_code.co_name -try: - import cairocffi as cairo -except ImportError: - try: - import cairo - except ImportError: - raise ImportError("Cairo backend requires that cairocffi or pycairo is installed.") - else: - HAS_CAIRO_CFFI = False -else: - HAS_CAIRO_CFFI = True - -_version_required = (1,2,0) -if cairo.version_info < _version_required: - raise ImportError ("Pycairo %d.%d.%d is installed\n" - "Pycairo %d.%d.%d or later is required" - % (cairo.version_info + _version_required)) -backend_version = cairo.version -del _version_required +from .cairo_compat import cairo, HAS_CAIRO_CFFI from matplotlib.backend_bases import RendererBase, GraphicsContextBase,\ FigureManagerBase, FigureCanvasBase diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 1aee5c75f590..870b239278a5 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -6,25 +6,7 @@ import os, sys def fn_name(): return sys._getframe(1).f_code.co_name -try: - import gi -except ImportError: - raise ImportError("Gtk3 backend requires pygobject to be installed.") - -try: - gi.require_version("Gtk", "3.0") -except AttributeError: - raise ImportError( - "pygobject version too old -- it must have require_version") -except ValueError: - raise ImportError( - "Gtk3 backend requires the GObject introspection bindings for Gtk 3 " - "to be installed.") - -try: - from gi.repository import Gtk, Gdk, GObject, GLib -except ImportError: - raise ImportError("Gtk3 backend requires pygobject to be installed.") +from .gtk3_compat import Gtk, Gdk, GObject, GLib import matplotlib from matplotlib._pylab_helpers import Gcf @@ -167,6 +149,12 @@ class FigureCanvasGTK3 (Gtk.DrawingArea, FigureCanvasBase): 65421 : 'enter', } + modifier_keys = [ + (Gdk.ModifierType.MOD4_MASK, 'super'), + (Gdk.ModifierType.MOD1_MASK, 'alt'), + (Gdk.ModifierType.CONTROL_MASK, 'ctrl'), + ] + # Setting this as a static constant prevents # this resulting expression from leaking event_mask = (Gdk.EventMask.BUTTON_PRESS_MASK | @@ -293,12 +281,7 @@ def _get_key(self, event): else: key = None - modifiers = [ - (Gdk.ModifierType.MOD4_MASK, 'super'), - (Gdk.ModifierType.MOD1_MASK, 'alt'), - (Gdk.ModifierType.CONTROL_MASK, 'ctrl'), - ] - for key_mask, prefix in modifiers: + for key_mask, prefix in self.modifier_keys: if event.state & key_mask: key = '{0}+{1}'.format(prefix, key) diff --git a/lib/matplotlib/backends/backend_gtk3agg.py b/lib/matplotlib/backends/backend_gtk3agg.py index c3eb1da68be3..9572dced3a8c 100644 --- a/lib/matplotlib/backends/backend_gtk3agg.py +++ b/lib/matplotlib/backends/backend_gtk3agg.py @@ -9,7 +9,7 @@ from . import backend_agg from . import backend_gtk3 -from .backend_cairo import cairo, HAS_CAIRO_CFFI +from .cairo_compat import cairo, HAS_CAIRO_CFFI from matplotlib.figure import Figure from matplotlib import transforms diff --git a/lib/matplotlib/backends/backend_gtk3cairo.py b/lib/matplotlib/backends/backend_gtk3cairo.py index da8f099be7f6..28af8e9e5c72 100644 --- a/lib/matplotlib/backends/backend_gtk3cairo.py +++ b/lib/matplotlib/backends/backend_gtk3cairo.py @@ -5,7 +5,7 @@ from . import backend_gtk3 from . import backend_cairo -from .backend_cairo import cairo, HAS_CAIRO_CFFI +from .cairo_compat import cairo, HAS_CAIRO_CFFI from matplotlib.figure import Figure class RendererGTK3Cairo(backend_cairo.RendererCairo): diff --git a/lib/matplotlib/backends/cairo_compat.py b/lib/matplotlib/backends/cairo_compat.py new file mode 100644 index 000000000000..0d6215beaaf7 --- /dev/null +++ b/lib/matplotlib/backends/cairo_compat.py @@ -0,0 +1,20 @@ +try: + import cairocffi as cairo +except ImportError: + try: + import cairo + except ImportError: + raise ImportError( + "Cairo backend requires that cairocffi or pycairo is installed.") + else: + HAS_CAIRO_CFFI = False +else: + HAS_CAIRO_CFFI = True + +_version_required = (1, 2, 0) +if cairo.version_info < _version_required: + raise ImportError("Pycairo %d.%d.%d is installed\n" + "Pycairo %d.%d.%d or later is required" + % (cairo.version_info + _version_required)) +backend_version = cairo.version +del _version_required diff --git a/lib/matplotlib/backends/gtk3_compat.py b/lib/matplotlib/backends/gtk3_compat.py new file mode 100644 index 000000000000..c3847041d4f6 --- /dev/null +++ b/lib/matplotlib/backends/gtk3_compat.py @@ -0,0 +1,34 @@ +import matplotlib + +error_msg_gtk3 = "Gtk3 backend requires the installation of pygobject or pgi." + +# Import the first library that works from the rcParam list +# throw ImportError if none works +for lib in matplotlib.rcParams['backend.gi_preference']: + try: + gi = __import__(lib, globals(), locals(), [], 0) + break + except ImportError: + pass +else: + raise ImportError(error_msg_gtk3) + +# Check version +try: + gi.require_version("Gtk", "3.0") +except AttributeError: + raise ImportError( + "pygobject version too old -- it must have require_version") +except ValueError: + raise ImportError( + "Gtk3 backend requires the installation of GObject introspection " + "bindings for Gtk 3") + +# cleanly import pkgs to global scope +try: + pkgs = ['Gtk', 'Gdk', 'GObject', 'GLib'] + name = gi.__name__ + '.repository' + _temp = __import__(name, globals(), locals(), pkgs, 0) + globals().update(dict((k, getattr(_temp, k)) for k in pkgs)) +except (ImportError, AttributeError): + raise ImportError(error_msg_gtk3) diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index fcbd7adfe313..6c92d3137771 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -410,6 +410,9 @@ def deprecate_axes_colorcycle(value): validate_stringlist = _listify_validator(six.text_type) validate_stringlist.__doc__ = 'return a list' +validate_gi_preference = _listify_validator(ValidateInStrings('backend.gi_preference', + 'backend.gi_preference', ['gi', 'pgi'])) + validate_orientation = ValidateInStrings( 'orientation', ['landscape', 'portrait']) @@ -888,6 +891,7 @@ def validate_animation_writer_path(p): 'backend_fallback': [True, validate_bool], # agg is certainly present 'backend.qt4': ['PyQt4', validate_qt4], 'backend.qt5': ['PyQt5', validate_qt5], + 'backend.gi_preference': [['gi', 'pgi'], validate_gi_preference], 'webagg.port': [8988, validate_int], 'webagg.open_in_browser': [True, validate_bool], 'webagg.port_retries': [50, validate_int], diff --git a/lib/matplotlib/tests/test_backend_gtk3.py b/lib/matplotlib/tests/test_backend_gtk3.py new file mode 100644 index 000000000000..47953432f7e6 --- /dev/null +++ b/lib/matplotlib/tests/test_backend_gtk3.py @@ -0,0 +1,60 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +from matplotlib.externals import six +from matplotlib.externals.six import unichr +from matplotlib import pyplot as plt +from matplotlib.testing.decorators import cleanup, switch_backend +from matplotlib.testing.decorators import knownfailureif +from matplotlib._pylab_helpers import Gcf +import copy + +try: + # mock in python 3.3+ + from unittest import mock +except ImportError: + import mock + +try: + from matplotlib.backends.gtk3_compat import Gtk, Gdk, GObject, GLib + HAS_GTK3 = True +except ImportError: + HAS_GTK3 = False + + +def simulate_key_press(canvas, key, modifiers=[]): + event = mock.Mock() + + keyval = [k for k, v in six.iteritems(canvas.keyvald) if v == key] + if keyval: + keyval = keyval[0] + else: + keyval = ord(key) + event.keyval = keyval + + event.state = 0 + for key_mask, prefix in canvas.modifier_keys: + if prefix in modifiers: + event.state |= key_mask + + canvas.key_press_event(None, event) + + +@cleanup +#@knownfailureif(not HAS_GTK3) +@switch_backend('GTK3Agg') +def test_fig_close(): + #save the state of Gcf.figs + init_figs = copy.copy(Gcf.figs) + + # make a figure using pyplot interface + fig = plt.figure() + + # simulate user pressing the close shortcut + #simulate_key_press(fig.canvas, 'w', ['ctrl']) + + plt.show() + + # assert that we have removed the reference to the FigureManager + # that got added by plt.figure() + assert(init_figs == Gcf.figs) From 2e8dc4e8f8104cfd1c0aa86b9d737aaa33d5288e Mon Sep 17 00:00:00 2001 From: OceanWolf Date: Fri, 30 Sep 2016 22:33:06 +0200 Subject: [PATCH 2/2] Fix rebase error and add checking to prevent segfaulting --- lib/matplotlib/backends/backend_gtk3agg.py | 2 +- lib/matplotlib/backends/backend_gtk3cairo.py | 2 +- lib/matplotlib/rcsetup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/backend_gtk3agg.py b/lib/matplotlib/backends/backend_gtk3agg.py index 9572dced3a8c..e8cbb5373257 100644 --- a/lib/matplotlib/backends/backend_gtk3agg.py +++ b/lib/matplotlib/backends/backend_gtk3agg.py @@ -46,7 +46,7 @@ def on_draw_event(self, widget, ctx): else: bbox_queue = self._bbox_queue - if HAS_CAIRO_CFFI: + if HAS_CAIRO_CFFI and not isinstance(ctx, cairo.Context): ctx = cairo.Context._from_pointer( cairo.ffi.cast('cairo_t **', id(ctx) + object.__basicsize__)[0], diff --git a/lib/matplotlib/backends/backend_gtk3cairo.py b/lib/matplotlib/backends/backend_gtk3cairo.py index 28af8e9e5c72..fe5d2d6192bf 100644 --- a/lib/matplotlib/backends/backend_gtk3cairo.py +++ b/lib/matplotlib/backends/backend_gtk3cairo.py @@ -10,7 +10,7 @@ class RendererGTK3Cairo(backend_cairo.RendererCairo): def set_context(self, ctx): - if HAS_CAIRO_CFFI: + if HAS_CAIRO_CFFI and not isinstance(ctx, cairo.Context): ctx = cairo.Context._from_pointer( cairo.ffi.cast( 'cairo_t **', diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 6c92d3137771..8a0c81eb406a 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -410,7 +410,7 @@ def deprecate_axes_colorcycle(value): validate_stringlist = _listify_validator(six.text_type) validate_stringlist.__doc__ = 'return a list' -validate_gi_preference = _listify_validator(ValidateInStrings('backend.gi_preference', +validate_gi_preference = _listify_validator(ValidateInStrings( 'backend.gi_preference', ['gi', 'pgi'])) validate_orientation = ValidateInStrings(