Skip to content

Fallback to pgi #4810

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions INSTALL
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions doc/glossary/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ Glossary
channel. PDF was designed in part as a next-generation document
format to replace postscript

pgi
`pgi <https://pypi.python.org/pypi/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 <http://www.pygtk.org/>` provides
python wrappers for the :term:`GTK` widgets library; unlike pygtk,
PyGObject wraps GTK3 instead of the now obsolete GTK2.

pygtk
`pygtk <http://www.pygtk.org/>`_ provides python wrappers for
the :term:`GTK` widgets library for use with the GTK or GTKAgg
Expand Down
9 changes: 9 additions & 0 deletions doc/users/whats_new/2015-07-30_pgi.rst
Original file line number Diff line number Diff line change
@@ -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.

1 change: 1 addition & 0 deletions lib/matplotlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
20 changes: 1 addition & 19 deletions lib/matplotlib/backends/backend_cairo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 8 additions & 25 deletions lib/matplotlib/backends/backend_gtk3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/backends/backend_gtk3agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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],
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/backends/backend_gtk3cairo.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

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):
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 **',
Expand Down
20 changes: 20 additions & 0 deletions lib/matplotlib/backends/cairo_compat.py
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions lib/matplotlib/backends/gtk3_compat.py
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Member

@NelleV NelleV Sep 30, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not very familiar with the import mechanisms of python. Can you explain why import lib as gi wouldn't work here and why not resorting to importlib isn't an option? My very little knowledge says that import > importlib >> __import__.
Right now, it looks like we are trying to be too smart about this.

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")
Copy link
Member

@NelleV NelleV Sep 30, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add the minimum required version here? It will be easier for a user if the errors specify which min version is supported.

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)
4 changes: 4 additions & 0 deletions lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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', ['gi', 'pgi']))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

backend.gi, similarly to backend.qt4.


validate_orientation = ValidateInStrings(
'orientation', ['landscape', 'portrait'])

Expand Down Expand Up @@ -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],
Expand Down
60 changes: 60 additions & 0 deletions lib/matplotlib/tests/test_backend_gtk3.py
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unintentionally commented out?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind -- I see your comment about this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, just for Travis testing, it makes it easier to spot import mistakes. With this line commented out, if everything goes well all but python 2.6 should pass. With this line in, it can mask import errors on builds that should pass.

I guess I could replace this with a knownfailiureif(sys.version_info < (2, 7)), which would ensure Travis always flags errors, but for people testing on their own machines without GTK3 installed it wouldn't work so well... Do you have an opinion on this?

Of course the middle way, perhaps we should add (or perhaps it already exists, but I haven't seen), an isTravis variable so we can a test can only fail on developers machines but not on Travis...

@switch_backend('GTK3Agg')
def test_fig_close():
#save the state of Gcf.figs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code in that function is not strictly pep8. There are spaces missing after comments.

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)