Repository: pyinstaller/pyinstaller

Issue:
Error importing matplotlib: No module named 'tornado'
Hi

I am running pyinstaller dev version on Fedora 22. I am trying to package a Kivy application that uses matplotlib.

I installed pyinstaller as follows:

`sudo -E pip3 install https://github.com/pyinstaller/pyinstaller/archive/develop.zip`

I installed Kivy by cloning the Kivy repository, building and installing it:

```
git clone https://github.com/kivy/kivy.git
cd kivy
python3 setup.py build_ext --inplace -f
sudo python3 setup.py install
```

I installed matplotlib using pip3:

```
sudo -E pip3 install --upgrade setuptools
sudo -E yum -y install freetype-devel
sudo -E pip3 install matplotlib
```

I installed the Kivy matplotlib backend using pip3 and Kivy garden:

```
sudo -E pip3 install kivy-garden
garden install matplotlib
```

My app runs fine from the installed locations.

I then run pyinstaller on it as follows:

```
$ pyinstaller --name gui_dist ~/trunk/
[pyinstaller_op.txt](https://github.com/pyinstaller/pyinstaller/files/165098/pyinstaller_op.txt)
gui/main.py
```

I see lots of warnings and errors regarding matplotlib, specifically:

```
17478 INFO:   Matplotlib backend "WebAgg": ignored
    Traceback (most recent call last):
  File "/usr/lib64/python3.4/site-packages/matplotlib/backends/backend_webagg.py", line 28, in <module>
    import tornado
ImportError: No module named 'tornado'
```

(I have attached the entire output as a separate file).

My problem seems similar to [this issue](https://github.com/pyinstaller/pyinstaller/issues/1190) which says it's fixed by [this pull request](https://github.com/pyinstaller/pyinstaller/pull/1194) so I hoped it was fixed (I'm using the latest development version).

Please can you advise me how to fix this problem?

Best regards

David
[pyinstaller_op.txt](https://github.com/pyinstaller/pyinstaller/files/165135/pyinstaller_op.txt)



Discussion:

Resolving patch:
diff --git a/PyInstaller/compat.py b/PyInstaller/compat.py
--- a/PyInstaller/compat.py
+++ b/PyInstaller/compat.py
@@ -14,6 +14,7 @@
 """
 
 
+import io
 import os
 import platform
 import site
@@ -80,6 +81,12 @@
                      'Please define constant PYDYLIB_NAMES for your platform.')
 
 
+# Function with which to open files. In Python 3, this is the open() built-in;
+# in Python 2, this is the Python 3 open() built-in backported into the "io"
+# module as io.open(). The Python 2 open() built-in is commonly regarded as
+# unsafe in regards to character encodings and hence inferior to io.open().
+open_file = open if is_py3 else io.open
+
 # In Python 3 there is exception FileExistsError. But it is not available
 # in Python 2. For Python 2 fall back to OSError exception.
 if is_py2:
@@ -271,14 +278,59 @@ def unsetenv(name):
 
 # Exec commands in subprocesses.
 
-
 def exec_command(*cmdargs, **kwargs):
     """
-    Wrap creating subprocesses
-
-    Return stdout of the invoked command. On Python 3, the 'encoding' kwarg controls
-    how the output is decoded to 'str'
+    Run the command specified by the passed positional arguments, optionally
+    configured by the passed keyword arguments.
+
+    .. DANGER::
+       **Ignore this function's return value** -- unless this command's standard
+       output contains _only_ pathnames, in which case this function returns the
+       correct filesystem-encoded string expected by PyInstaller. In all other
+       cases, this function's return value is _not_ safely usable. Consider
+       calling the general-purpose `exec_command_stdout()` function instead.
+
+       For backward compatibility, this function's return value non-portably
+       depends on the current Python version and passed keyword arguments:
+
+       * Under Python 2.7, this value is an **encoded `str` string** rather than
+         a decoded `unicode` string. This value _cannot_ be safely used for any
+         purpose (e.g., string manipulation or parsing), except to be passed
+         directly to another non-Python command.
+       * Under Python 3.x, this value is a **decoded `str` string**. However,
+         even this value is _not_ necessarily safely usable:
+         * If the `encoding` parameter is passed, this value is guaranteed to be
+           safely usable.
+         * Else, this value _cannot_ be safely used for any purpose (e.g.,
+           string manipulation or parsing), except to be passed directly to
+           another non-Python command. Why? Because this value has been decoded
+           with the encoding specified by `sys.getfilesystemencoding()`, the
+           encoding used by `os.fsencode()` and `os.fsdecode()` to convert from
+           platform-agnostic to platform-specific pathnames. This is _not_
+           necessarily the encoding with which this command's standard output
+           was encoded. Cue edge-case decoding exceptions.
+
+    Parameters
+    ----------
+    cmdargs : list
+        Variadic list whose:
+        1. Mandatory first element is the absolute path, relative path,
+           or basename in the current `${PATH}` of the command to run.
+        1. Optional remaining elements are arguments to pass to this command.
+    encoding : str, optional
+        Optional keyword argument specifying the encoding with which to decode
+        this command's standard output under Python 3. As this function's return
+        value should be ignored, this argument should _never_ be passed.
+
+    All remaining keyword arguments are passed as is to the `subprocess.Popen()`
+    constructor.
+
+    Returns
+    ----------
+    str
+        Ignore this value. See discussion above.
     """
+
     encoding = kwargs.pop('encoding', None)
     out = subprocess.Popen(cmdargs, stdout=subprocess.PIPE, **kwargs).communicate()[0]
     # Python 3 returns stdout/stderr as a byte array NOT as string.
@@ -297,10 +349,26 @@ def exec_command(*cmdargs, **kwargs):
 
 def exec_command_rc(*cmdargs, **kwargs):
     """
-    Wrap creating subprocesses.
-
-    Return exit code of the invoked command.
+    Return the exit code of the command specified by the passed positional
+    arguments, optionally configured by the passed keyword arguments.
+
+    Parameters
+    ----------
+    cmdargs : list
+        Variadic list whose:
+        1. Mandatory first element is the absolute path, relative path,
+           or basename in the current `${PATH}` of the command to run.
+        1. Optional remaining elements are arguments to pass to this command.
+
+    All keyword arguments are passed as is to the `subprocess.call()` function.
+
+    Returns
+    ----------
+    int
+        This command's exit code as an unsigned byte in the range `[0, 255]`,
+        where 0 signifies success and all other values failure.
     """
+
     # 'encoding' keyword is not supported for 'subprocess.call'.
     # Remove it thus from kwargs.
     if 'encoding' in kwargs:
@@ -308,13 +376,96 @@ def exec_command_rc(*cmdargs, **kwargs):
     return subprocess.call(cmdargs, **kwargs)
 
 
-def exec_command_all(*cmdargs, **kwargs):
+def exec_command_stdout(*command_args, **kwargs):
+    """
+    Capture and return the standard output of the command specified by the
+    passed positional arguments, optionally configured by the passed keyword
+    arguments.
+
+    Unlike the legacy `exec_command()` and `exec_command_all()` functions, this
+    modern function is explicitly designed for cross-platform portability. The
+    return value may be safely used for any purpose, including string
+    manipulation and parsing.
+
+    .. NOTE::
+       If this command's standard output contains _only_ pathnames, this
+       function does _not_ return the correct filesystem-encoded string expected
+       by PyInstaller. If this is the case, consider calling the
+       filesystem-specific `exec_command()` function instead.
+
+    Parameters
+    ----------
+    cmdargs : list
+        Variadic list whose:
+        1. Mandatory first element is the absolute path, relative path,
+           or basename in the current `${PATH}` of the command to run.
+        1. Optional remaining elements are arguments to pass to this command.
+    encoding : str, optional
+        Optional name of the encoding with which to decode this command's
+        standard output (e.g., `utf8`), passed as a keyword argument. If
+        unpassed , this output will be decoded in a portable manner specific to
+        to the current platform, shell environment, and system settings with
+        Python's built-in `universal_newlines` functionality.
+
+    All remaining keyword arguments are passed as is to the
+    `subprocess.check_output()` function.
+
+    Returns
+    ----------
+    unicode or str
+        Unicode string of this command's standard output decoded according to
+        the "encoding" keyword argument. This string's type depends on the
+        current Python version as follows:
+        * Under Python 2.7, this is a decoded `unicode` string.
+        * Under Python 3.x, this is a decoded `str` string.
     """
-    Wrap creating subprocesses
 
-    Return tuple (exit_code, stdout, stderr) of the invoked command.
+    # Value of the passed "encoding" parameter, defaulting to None.
+    encoding = kwargs.pop('encoding', None)
+
+    # If no encoding was specified, the current locale is defaulted to. Else, an
+    # encoding was specified. To ensure this encoding is respected, the
+    # "universal_newlines" option is disabled if also passed. Nice, eh?
+    kwargs['universal_newlines'] = encoding is None
+
+    # Standard output captured from this command as a decoded Unicode string if
+    # "universal_newlines" is enabled or an encoded byte array otherwise.
+    stdout = subprocess.check_output(command_args, **kwargs)
+
+    # Return a Unicode string, decoded from this encoded byte array if needed.
+    return stdout if encoding is None else stdout.decode(encoding)
 
-    On Python 3, the 'encoding' kwarg controls how stdout and stderr are decoded to 'str'
+
+def exec_command_all(*cmdargs, **kwargs):
+    """
+    Run the command specified by the passed positional arguments, optionally
+    configured by the passed keyword arguments.
+
+    .. DANGER::
+       **Ignore this function's return value.** If this command's standard
+       output consists solely of pathnames, consider calling `exec_command()`;
+       else, consider calling `exec_command_stdout()`.
+
+    Parameters
+    ----------
+    cmdargs : list
+        Variadic list whose:
+        1. Mandatory first element is the absolute path, relative path,
+           or basename in the current `${PATH}` of the command to run.
+        1. Optional remaining elements are arguments to pass to this command.
+    encoding : str, optional
+        Optional keyword argument specifying the encoding with which to decode
+        this command's standard output under Python 3. As this function's return
+        value should be ignored, this argument should _never_ be passed.
+
+    All remaining keyword arguments are passed as is to the `subprocess.Popen()`
+    constructor.
+
+    Returns
+    ----------
+    (int, str, str)
+        Ignore this 3-element tuple `(exit_code, stdout, stderr)`. See the
+        `exec_command()` function for discussion.
     """
     proc = subprocess.Popen(cmdargs, bufsize=-1,  # Default OS buffer size.
             stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
@@ -363,7 +514,7 @@ def __wrap_python(args, kwargs):
         env['PYTHONIOENCODING'] = 'UTF-8'
         # ... and ensure we read output as utf-8
         kwargs['encoding'] = 'UTF-8'
-     
+
     return cmdargs, kwargs
 
 
@@ -397,6 +548,8 @@ def exec_python_all(*args, **kwargs):
     return exec_command_all(*cmdargs, **kwargs)
 
 
+## Path handling.
+
 # The function os.getcwd() in Python 2 does not work with unicode paths on Windows.
 def getcwd():
     """
@@ -430,6 +583,76 @@ def expand_path(path):
     return os.path.expandvars(os.path.expanduser(path))
 
 
+# Define the shutil.which() function, first introduced by Python 3.3.
+try:
+    from shutil import which
+# If undefined, this is Python 2.7. For compatibility, this function has been
+# backported without modification from the most recent stable version of
+# Python as of this writing: Python 3.5.1.
+except ImportError:
+    def which(cmd, mode=os.F_OK | os.X_OK, path=None):
+        """Given a command, mode, and a PATH string, return the path which
+        conforms to the given mode on the PATH, or None if there is no such
+        file.
+
+        `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
+        of os.environ.get("PATH"), or can be overridden with a custom search
+        path.
+
+        """
+        # Check that a given file can be accessed with the correct mode.
+        # Additionally check that `file` is not a directory, as on Windows
+        # directories pass the os.access check.
+        def _access_check(fn, mode):
+            return (os.path.exists(fn) and os.access(fn, mode)
+                    and not os.path.isdir(fn))
+
+        # If we're given a path with a directory part, look it up directly rather
+        # than referring to PATH directories. This includes checking relative to the
+        # current directory, e.g. ./script
+        if os.path.dirname(cmd):
+            if _access_check(cmd, mode):
+                return cmd
+            return None
+
+        if path is None:
+            path = os.environ.get("PATH", os.defpath)
+        if not path:
+            return None
+        path = path.split(os.pathsep)
+
+        if sys.platform == "win32":
+            # The current directory takes precedence on Windows.
+            if not os.curdir in path:
+                path.insert(0, os.curdir)
+
+            # PATHEXT is necessary to check on Windows.
+            pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
+            # See if the given file matches any of the expected path extensions.
+            # This will allow us to short circuit when given "python.exe".
+            # If it does match, only test that one, otherwise we have to try
+            # others.
+            if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
+                files = [cmd]
+            else:
+                files = [cmd + ext for ext in pathext]
+        else:
+            # On other platforms you don't have things like PATHEXT to tell you
+            # what file suffixes are executable, so just pass on cmd as-is.
+            files = [cmd]
+
+        seen = set()
+        for dir in path:
+            normdir = os.path.normcase(dir)
+            if not normdir in seen:
+                seen.add(normdir)
+                for thefile in files:
+                    name = os.path.join(dir, thefile)
+                    if _access_check(name, mode):
+                        return name
+        return None
+
+
 # Obsolete command line options.
 
 class __obsolete_option:
@@ -453,7 +676,7 @@ def __add_obsolete_options(parser):
     print error message when they are present.
     """
     g = parser.add_argument_group('Obsolete options (not used anymore)')
-    g.add_argument(*_OLD_OPTIONS, 
+    g.add_argument(*_OLD_OPTIONS,
                    action=__obsolete_option,
                    help='These options do not exist anymore.')
 
diff --git a/PyInstaller/hooks/hook-gi.repository.GdkPixbuf.py b/PyInstaller/hooks/hook-gi.repository.GdkPixbuf.py
--- a/PyInstaller/hooks/hook-gi.repository.GdkPixbuf.py
+++ b/PyInstaller/hooks/hook-gi.repository.GdkPixbuf.py
@@ -7,7 +7,7 @@
 # The full license is in the file COPYING.txt, distributed with this software.
 #-----------------------------------------------------------------------------
 """
-Import hook for PyGObject https://wiki.gnome.org/PyGObject
+Import hook for PyGObject's "gi.repository.GdkPixbuf" package.
 """
 
 import glob
@@ -15,46 +15,85 @@
 import subprocess
 
 from PyInstaller.config import CONF
-from PyInstaller.compat import is_darwin, is_win, is_linux, exec_command
-from PyInstaller.utils.hooks import collect_glib_translations, get_gi_typelibs,\
-    get_gi_libdir
-
-binaries, datas, hiddenimports = get_gi_typelibs('GdkPixbuf', '2.0')
-datas += collect_glib_translations('gdk-pixbuf')
-
-libdir = get_gi_libdir('GdkPixbuf', '2.0')
-if is_win:
-    pattern = os.path.join(libdir, 'gdk-pixbuf-2.0', '2.10.0', 'loaders', '*.dll')
-elif is_darwin or is_linux:
-    pattern = os.path.join(libdir, 'gdk-pixbuf-2.0', '2.10.0', 'loaders', '*.so')
-
-if pattern:
-    for f in glob.glob(pattern):
-        binaries.append((f, 'lib/gdk-pixbuf-2.0/2.10.0/loaders'))
-    
-    # Create an updated version of the loader cache
-    cachedata = exec_command('gdk-pixbuf-query-loaders')
-    
-    if is_darwin:
-        cd = []
-        prefix = '"' + libdir
-        plen = len(prefix)
-        
-        for line in cachedata.splitlines():
-            if line.startswith('#'):
-                continue
-            if line.startswith(prefix):
-                line = '"@executable_path/lib' + line[plen:]
-            cd.append(line)
-            
-        cachedata = '\n'.join(cd)
-    
-    cachefile = os.path.join(CONF['workpath'], 'loaders.cache')
-    with open(cachefile, 'w') as fp:
-        fp.write(cachedata)
-    
-    datas.append((cachefile, 'lib/gdk-pixbuf-2.0/2.10.0'))
+from PyInstaller.compat import (
+    exec_command_stdout, is_darwin, is_win, is_linux, open_file, which)
+from PyInstaller.utils.hooks import (
+    collect_glib_translations, get_gi_typelibs, get_gi_libdir, logger)
+
+# If the "gdk-pixbuf-query-loaders" command is not in the current ${PATH}, GDK
+# and thus GdkPixbuf is unavailable. Return with a non-fatal warning.
+if which('gdk-pixbuf-query-loaders') is None:
+    logger.warn(
+        '"hook-gi.repository.GdkPixbuf" ignored, since GDK not found '
+        '(i.e., "gdk-pixbuf-query-loaders" not in $PATH).'
+    )
+# Else, GDK is available. Let's do this.
 else:
-    # To add a new platform add a new elif above with the proper is_<platform> and
-    # proper pattern for finding the loaders on your platform.
-    logger.warn('Bundling GdkPixbuf loaders is currently not supported on your platform.')
+    binaries, datas, hiddenimports = get_gi_typelibs('GdkPixbuf', '2.0')
+    datas += collect_glib_translations('gdk-pixbuf')
+
+    libdir = get_gi_libdir('GdkPixbuf', '2.0')
+
+    # To add support for a new platform, add a new "elif" branch below with the
+    # proper is_<platform>() test and glob for finding loaders on that platform.
+    if is_win:
+        pattern = os.path.join(
+            libdir, 'gdk-pixbuf-2.0', '2.10.0', 'loaders', '*.dll')
+    elif is_darwin or is_linux:
+        pattern = os.path.join(
+            libdir, 'gdk-pixbuf-2.0', '2.10.0', 'loaders', '*.so')
+
+    # If loader detection is supported on this platform, bundle all detected
+    # loaders and an updated loader cache.
+    if pattern:
+        # Bundle all found loaders with this user application.
+        for f in glob.glob(pattern):
+            binaries.append((f, 'lib/gdk-pixbuf-2.0/2.10.0/loaders'))
+
+        # Filename of the loader cache to be written below.
+        cachefile = os.path.join(CONF['workpath'], 'loaders.cache')
+
+        # Run the "gdk-pixbuf-query-loaders" command and capture its standard
+        # output providing an updated loader cache; then write this output to
+        # the loader cache bundled with this frozen application.
+        #
+        # If the current platform is OS X...
+        if is_darwin:
+            # To permit string munging, decode the encoded bytes output by this
+            # command (i.e., enable the "universal_newlines" option). Note that:
+            #
+            # * Under Python 2.7, "cachedata" will be a decoded "unicode" object.
+            # * Under Python 3.x, "cachedata" will be a decoded "str" object.
+            cachedata = exec_command_stdout('gdk-pixbuf-query-loaders')
+
+            cd = []
+            prefix = '"' + libdir
+            plen = len(prefix)
+
+            # For each line in the updated loader cache...
+            for line in cachedata.splitlines():
+                if line.startswith('#'):
+                    continue
+                if line.startswith(prefix):
+                    line = '"@executable_path/lib' + line[plen:]
+                cd.append(line)
+
+            # Rejoin these lines in a manner preserving this object's "unicode"
+            # type under Python 2.
+            cachedata = u'\n'.join(cd)
+
+            # Write the updated loader cache to this file.
+            with open_file(cachefile, 'w') as fp:
+                fp.write(cachedata)
+        # Else, the current platform is *NOT* OS X. In this case, no changes to
+        # the loader cache are required. For efficiency and reliability, this
+        # command's encoded byte output is written as is without being decoded.
+        else:
+            with open_file(cachefile, 'wb') as fp:
+                fp.write(subprocess.check_output('gdk-pixbuf-query-loaders'))
+
+        # Bundle this loader cache with this frozen application.
+        datas.append((cachefile, 'lib/gdk-pixbuf-2.0/2.10.0'))
+    # Else, loader detection is unsupported on this platform.
+    else:
+        logger.warn('GdkPixbuf loader bundling unsupported on your platform.')


Test patch:
diff --git a/PyInstaller/utils/tests.py b/PyInstaller/utils/tests.py
--- a/PyInstaller/utils/tests.py
+++ b/PyInstaller/utils/tests.py
@@ -15,9 +15,11 @@
 import sys
 import traceback
 import pytest
+from _pytest.runner import Skipped
 from PyInstaller.compat import is_darwin, is_win, is_py2, is_py3
 
 # Wrap some pytest decorators to be consistent in tests.
+parametrize = pytest.mark.parametrize
 skipif = pytest.mark.skipif
 skipif_notwin = skipif(not is_win, reason='requires Windows')
 skipif_notosx = skipif(not is_darwin, reason='requires Mac OS X')
@@ -28,41 +30,21 @@
 xfail_py3 = xfail(is_py3, reason='fails with Python 3')
 
 
-# TODO: Rename to importerskip(). That said, is even that really the best name?
-# skipif_modules_not_found() or something similar would probably be more
-# self-explanatory.
-def importorskip(*modules):
-    """
-    This wraps the pytest decorator to evaluate all modules that are required
-    for running a test.
-
-    :param modules: Module names
-    :return: pytest decorator with a reason and list of required modules.
-    """
-    mods_avail = True
-    for m in modules:
-        try:
-            __import__(m)
-        except (ImportError, SyntaxError):
-            mods_avail = False
-        # Catch any other exception and print it with stacktrace to stderr.
-        # This should allow to run other tests when there is something wrong
-        # with the imported module.
-        except Exception as e:
-            mods_avail = False
-            print("importorskip: Exception in module '%s':" % m)
-            print('-'*60)
-            traceback.print_exc(file=sys.stdout)
-            print('-'*60)
-    # Return pytest decorator.
-    return skipif(not mods_avail,
-                  reason='requires modules %s' % ', '.join(modules))
-
-
 def skip(reason):
     """
     Unconditionally skip the currently decorated test with the passed reason.
 
+    This decorator is intended to be called either directly as a function _or_
+    indirectly as a decorator. This differs from both:
+
+    * `pytest.skip()`, intended to be called only directly as a function.
+      Attempting to call this function indirectly as a decorator produces
+      extraneous ignorable messages on standard output resembling
+      `SKIP [1] PyInstaller/utils/tests.py:65: could not import 'win32com'`.
+    * `pytest.mark.skip()`, intended to be called only indirectly as a
+      decorator. Attempting to call this decorator directly as a function
+      reduces to a noop.
+
     Parameters
     ----------
     reason : str
@@ -70,3 +52,60 @@ def skip(reason):
     """
 
     return skipif(True, reason=reason)
+
+
+def importorskip(modname, minversion=None):
+    """
+    This decorator skips the currently decorated test if the module with the
+    passed name is unimportable _or_ importable but of a version less than the
+    passed minimum version if any.
+
+    This decorator's name is intentionally mispelled as `importerskip` rather
+    than `importerskip` to coincide with the `pytest.importorskip()` function
+    internally called by this decorator.
+
+    Parameters
+    ----------
+    modname : str
+        Fully-qualified name of the module required by this test.
+    minversion : str
+        Optional minimum version of this module as a string (e.g., `3.14.15`)
+        required by this test _or_ `None` if any module version is acceptable.
+        Defaults to `None`.
+
+    Returns
+    ----------
+    pytest.skipif
+        Decorator describing these requirements if unmet _or_ the identity
+        decorator otherwise (i.e., if these requirements are met).
+    """
+
+    # Defer to the eponymous function of the same name.
+    try:
+        pytest.importorskip(modname, minversion)
+    # Silently convert expected import and syntax errors into @skip decoration.
+    except Skipped as exc:
+        return skip(str(exc))
+    # Convert all other unexpected errors into the same decoration.
+    except Exception as exc:
+        # For debuggability, print a verbose stacktrace.
+        print('importorskip: Exception in module "{}":'.format(modname))
+        print('-' * 60)
+        traceback.print_exc(file=sys.stdout)
+        print('-' * 60)
+
+        return skip(str(exc))
+    # Else, this module is importable and optionally satisfies this minimum
+    # version. Reduce this decoration to a noop.
+    else:
+        return _noop
+
+
+def _noop(object):
+    """
+    Return the passed object unmodified.
+
+    This private function is intended to be used as the identity decorator.
+    """
+
+    return object
diff --git a/tests/functional/test_hooks/test_gi.py b/tests/functional/test_hooks/test_gi.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/test_hooks/test_gi.py
@@ -0,0 +1,66 @@
+#-----------------------------------------------------------------------------
+# Copyright (c) 2005-2016, PyInstaller Development Team.
+#
+# Distributed under the terms of the GNU General Public License with exception
+# for distributing bootloader.
+#
+# The full license is in the file COPYING.txt, distributed with this software.
+#-----------------------------------------------------------------------------
+
+"""
+Functional tests for PyGObject.
+"""
+
+from PyInstaller.utils.tests import importorskip, parametrize
+
+
+## For PyGObject >= 1.0
+
+# Test the usability of "gi.repository" packages provided by PyGObject >= 1.0.
+@importorskip('gi.repository.Gst')
+def test_gi_gst_binding(pyi_builder):
+    pyi_builder.test_source('''
+        import gi
+        gi.require_version('Gst', '1.0')
+        from gi.repository import Gst
+        Gst.init(None)
+        print(Gst)
+    ''')
+
+
+## For PyGObject >= 2.0
+
+# Names of all "gi.repository" packages provided by PyGObject >= 2.0 to be
+# tested below, typically corresponding to those packages hooked by PyInstaller.
+gi2_repository_names = ['GLib', 'GModule', 'GObject', 'GdkPixbuf', 'Gio',]
+
+# Names of the same packages, decorated to be skipped if unimportable.
+gi2_repository_names_skipped_if_unimportable = [
+    importorskip('gi.repository.' + gi2_repository_name)(gi2_repository_name)
+    for gi2_repository_name in gi2_repository_names
+]
+
+# Test the usability of "gi.repository" packages provided by PyGObject >= 2.0.
+# For simplicity, these tests are parametrized as
+# "test_gi2_repository[{one_type}-{repository_name}]" (e.g.,
+# "test_gi2_repository[onedir-GdkPixbuf]").
+@importorskip('gi.repository')
+@parametrize(
+    'repository_name',
+    gi2_repository_names_skipped_if_unimportable,
+    # Ensure human-readable test parameter names.
+    ids=gi2_repository_names)
+def test_gi2_repository(pyi_builder, repository_name):
+    '''
+    Test the importability of the `gi.repository` subpackage with the passed
+    name installed with PyGObject >= 2.0 (e.g., `GLib`, corresponding to the
+    `gi.repository.GLib` subpackage).
+    '''
+
+    # Test the importability of this subpackage.
+    pyi_builder.test_source('''
+        import gi
+        gi.require_version('{repository_name}', '2.0')
+        from gi.repository import {repository_name}
+        print({repository_name})
+        '''.format(repository_name=repository_name))
diff --git a/tests/functional/test_hooks/test_matplotlib.py b/tests/functional/test_hooks/test_matplotlib.py
new file mode 100644
--- /dev/null
+++ b/tests/functional/test_hooks/test_matplotlib.py
@@ -0,0 +1,123 @@
+#-----------------------------------------------------------------------------
+# Copyright (c) 2005-2016, PyInstaller Development Team.
+#
+# Distributed under the terms of the GNU General Public License with exception
+# for distributing bootloader.
+#
+# The full license is in the file COPYING.txt, distributed with this software.
+#-----------------------------------------------------------------------------
+
+"""
+Functional tests for Matplotlib.
+"""
+
+import pytest
+from PyInstaller.utils.tests import importorskip
+
+
+# List of 4-tuples "(backend_name, package_name, rcParams_key, rcParams_value)",
+# where:
+#
+# * "backend_name" is the name of a Matplotlib backend to be tested below.
+# * "package_name" is the name of the external package required by this backend.
+# * "rcParams_key" is the name of a Matplotlib parameter to be set before testing
+#   this backend or "None" if no parameter is to be set.
+# * "rcParams_value" is the value to set that parameter to if "rcParams_key" is
+#   not "None" or ignored otherwise.
+backend_rcParams_key_values = [
+    # PySide.
+    ('Qt4Agg', 'PySide', 'backend.qt4', 'PySide'),
+    # PyQt4.
+    ('Qt4Agg', 'PyQt4', 'backend.qt4', 'PyQt4'),
+    # PyQt5.
+    ('Qt5Agg', 'PyQt5', 'backend.qt5', 'PyQt5'),
+]
+
+# Same list, decorated to skip all backends whose packages are unimportable.
+backend_rcParams_key_values_skipped_if_unimportable = [
+    importorskip(backend_rcParams_key_value[1])(backend_rcParams_key_value)
+    for backend_rcParams_key_value in backend_rcParams_key_values
+]
+
+# Names of all packages required by backends listed above.
+package_names = [
+    backend_rcParams_key_value[1]
+    for backend_rcParams_key_value in backend_rcParams_key_values
+]
+
+# Test Matplotlib with access to only one backend at a time.
+@importorskip('matplotlib')
+@pytest.mark.parametrize(
+    'backend_name, package_name, rcParams_key, rcParams_value',
+    backend_rcParams_key_values_skipped_if_unimportable,
+    ids=package_names)
+def test_matplotlib(
+    pyi_builder, backend_name, package_name, rcParams_key, rcParams_value):
+    '''
+    Test Matplotlib with the passed backend enabled, the passed backend package
+    included with this frozen application, all other backend packages explicitly
+    excluded from this frozen application, and the passed rcParam key set to the
+    corresponding passed value if that key is _not_ `None` or ignore that value
+    otherwise.
+    '''
+
+    # PyInstaller options excluding all backend packages except the passed
+    # backend package. This is especially critical for Qt backend packages
+    # (e.g., "PyQt4", "PySide"). On first importation, Matplotlib attempts to
+    # import all available Qt packages. However, runtime PyInstaller hooks fail
+    # when multiple Qt packages are frozen into the same application. For each
+    # such package, all other Qt packages must be excluded.
+    pyi_args = [
+        '--exclude-module=' + package_name_excludable
+        for package_name_excludable in package_names
+        if  package_name_excludable != package_name
+    ]
+
+    # Script to be tested, enabling this Qt backend.
+    test_script = ('''
+    import matplotlib, os, sys, tempfile
+
+    # Localize test parameters.
+    backend_name = {backend_name!r}
+    rcParams_key = {rcParams_key!r}
+    rcParams_value = {rcParams_value!r}
+
+    # Report these parameters.
+    print('Testing Matplotlib with:\\n'
+        '\\tbackend: {{}}\\n'
+        '\\trcParams:\\n'
+        '\\t\\tkey: {{}}\\n'
+        '\\t\\tvalue: {{}}'.format(
+        backend_name, rcParams_key, rcParams_value))
+
+    # Configure Matplotlib *BEFORE* calling any Matplotlib functions.
+    matplotlib.rcParams[rcParams_key] = rcParams_value
+
+    # Enable the desired backend *BEFORE* plotting with this backend.
+    matplotlib.use(backend_name)
+
+    # A runtime hook should force Matplotlib to create its configuration
+    # directory in a temporary directory rather than in "$HOME/.matplotlib".
+    configdir = os.environ['MPLCONFIGDIR']
+    print('MPLCONFIGDIR: %s' % configdir)
+    if not configdir.startswith(tempfile.gettempdir()):
+        raise SystemExit('MPLCONFIGDIR not pointing to temp directory.')
+
+    # Matplotlib's data directory should point to sys._MEIPASS.
+    datadir = os.environ['MATPLOTLIBDATA']
+    print('MATPLOTLIBDATA: %s' % datadir)
+    if not datadir.startswith(sys._MEIPASS):
+        raise SystemExit('MATPLOTLIBDATA not pointing to sys._MEIPASS.')
+
+    # Test access to the standard "mpl_toolkits" namespace package installed
+    # with Matplotlib. Note that this import was reported to fail under
+    # Matplotlib 1.3.0.
+    from mpl_toolkits import axes_grid1
+    '''.format(
+        backend_name=backend_name,
+        rcParams_key=rcParams_key,
+        rcParams_value=rcParams_value,
+    ))
+
+    # Test this script.
+    pyi_builder.test_source(test_script, pyi_args=pyi_args)
diff --git a/tests/functional/test_libraries.py b/tests/functional/test_libraries.py
--- a/tests/functional/test_libraries.py
+++ b/tests/functional/test_libraries.py
@@ -493,15 +493,27 @@ def testit():
 def test_scipy(pyi_builder):
     pyi_builder.test_source(
         """
-        # General SciPy import.
+        from distutils.version import LooseVersion
+
+        # Test top-level SciPy importability.
+        import scipy
         from scipy import *
-        # Test import hooks for the following modules.
+
+        # Test hooked SciPy modules.
         import scipy.io.matlab
         import scipy.sparse.csgraph
-        # Some other "problematic" scipy submodules.
-        import scipy.lib
+
+        # Test problematic SciPy modules.
         import scipy.linalg
         import scipy.signal
+
+        # SciPy >= 0.16 privatized the previously public "scipy.lib" package as
+        # "scipy._lib". Since this package is problematic, test its
+        # importability regardless of SciPy version.
+        if LooseVersion(scipy.__version__) >= LooseVersion('0.16.0'):
+            import scipy._lib
+        else:
+            import scipy.lib
         """)
 
 
@@ -538,43 +550,6 @@ def test_twisted(pyi_builder):
             raise SystemExit('Twisted reactor not properly initialized.')
         """)
 
-# matplotlib tries to import any of PyQt4, PyQt5 or PySide. But if we
-# have more then one of these in the frozen app, the app's
-# runtime-hooks will crash. Thus we need to exclude the other two.
-all_qt_pkgs = ['PyQt4', 'PyQt5', 'PySide']
-excludes = []
-for pkg in all_qt_pkgs:
-    p = [p for p in all_qt_pkgs]
-    p = importorskip(pkg)(p)
-    excludes.append(p)
-
-@importorskip('matplotlib')
-@pytest.mark.parametrize("excludes", excludes, ids=all_qt_pkgs)
-def test_matplotlib(pyi_builder, excludes):
-    pyi_args = ['--exclude-module=%s' % e for e in excludes]
-    pyi_builder.test_source(
-        """
-        import os
-        import matplotlib
-        import sys
-        import tempfile
-        # In frozen state rthook should force matplotlib to create config directory
-        # in temp directory and not $HOME/.matplotlib.
-        configdir = os.environ['MPLCONFIGDIR']
-        print(('MPLCONFIGDIR: %s' % configdir))
-        if not configdir.startswith(tempfile.gettempdir()):
-            raise SystemExit('MPLCONFIGDIR not pointing to temp directory.')
-        # matplotlib data directory should point to sys._MEIPASS.
-        datadir = os.environ['MATPLOTLIBDATA']
-        print(('MATPLOTLIBDATA: %s' % datadir))
-        if not datadir.startswith(sys._MEIPASS):
-            raise SystemExit('MATPLOTLIBDATA not pointing to sys._MEIPASS.')
-        # This import was reported to fail with matplotlib 1.3.0.
-        from mpl_toolkits import axes_grid1
-        """, pyi_args=pyi_args)
-
-del all_qt_pkgs, excludes
-
 
 @importorskip('pyexcelerate')
 @pytest.mark.xfail(reason='TODO - known to fail')
@@ -608,62 +583,6 @@ def test_usb(pyi_builder):
         """)
 
 
-@importorskip('gi.repository.Gio')
-def test_gi_gio_binding(pyi_builder):
-    pyi_builder.test_source(
-        """
-        import gi
-        gi.require_version('Gio', '2.0')
-        from gi.repository import Gio
-        print(Gio)
-        """)
-
-
-@importorskip('gi.repository.GLib')
-def test_gi_glib_binding(pyi_builder):
-    pyi_builder.test_source(
-        """
-        import gi
-        gi.require_version('GLib', '2.0')
-        from gi.repository import GLib
-        print(GLib)
-        """)
-
-
-@importorskip('gi.repository.GModule')
-def test_gi_gmodule_binding(pyi_builder):
-    pyi_builder.test_source(
-        """
-        import gi
-        gi.require_version('GModule', '2.0')
-        from gi.repository import GModule
-        print(GModule)
-        """)
-
-
-@importorskip('gi.repository.GObject')
-def test_gi_gobject_binding(pyi_builder):
-    pyi_builder.test_source(
-        """
-        import gi
-        gi.require_version('GObject', '2.0')
-        from gi.repository import GObject
-        print(GObject)
-        """)
-
-
-@importorskip('gi.repository.Gst')
-def test_gi_gst_binding(pyi_builder):
-    pyi_builder.test_source(
-        """
-        import gi
-        gi.require_version('Gst', '1.0')
-        from gi.repository import Gst
-        Gst.init(None)
-        print(Gst)
-        """)
-
-
 @importorskip('PIL')
 @pytest.mark.xfail(reason="Fails with Pillow 3.0.0")
 def test_pil_img_conversion(pyi_builder_spec):
@@ -671,7 +590,8 @@ def test_pil_img_conversion(pyi_builder_spec):
 
 
 @xfail(is_darwin, reason='Issue #1895.')
-@importorskip('PIL', 'FixTk')
+@importorskip('PIL')
+@importorskip('FixTk')
 def test_pil_FixTk(pyi_builder):
     # hook-PIL is excluding FixTk, but is must still be included
     # since it is imported elsewhere. Also see issue #1584.
@@ -684,7 +604,8 @@ def test_pil_FixTk(pyi_builder):
     import FixTk, PIL
     """)
 
-@importorskip('PIL.ImageQt', 'PyQt5')
+@importorskip('PIL.ImageQt')
+@importorskip('PyQt5')
 def test_pil_PyQt5(pyi_builder):
     # hook-PIL is excluding PyQt5, but is must still be included
     # since it is imported elsewhere. Also see issue #1584.
@@ -694,7 +615,8 @@ def test_pil_PyQt5(pyi_builder):
     import PIL.ImageQt
     """)
 
-@importorskip('PIL.ImageQt', 'PyQt4')
+@importorskip('PIL.ImageQt')
+@importorskip('PyQt4')
 def test_pil_PyQt4(pyi_builder):
     # hook-PIL is excluding PyQt4, but is must still be included
     # since it is imported elsewhere. Also see issue #1584.