1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
|
#-----------------------------------------------------------------------------
# Copyright (c) 2013-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
from PyInstaller.compat import is_darwin
from PyInstaller.utils.hooks import logger, get_hook_config
from PyInstaller import isolated
@isolated.decorate
def _get_configured_default_backend():
"""
Return the configured default matplotlib backend name, if available as matplotlib.rcParams['backend'] (or overridden
by MPLBACKEND environment variable. If the value of matplotlib.rcParams['backend'] corresponds to the auto-sentinel
object, returns None
"""
import matplotlib
# matplotlib.rcParams overrides the __getitem__ implementation and attempts to determine and load the default
# backend using pyplot.switch_backend(). Therefore, use dict.__getitem__().
val = dict.__getitem__(matplotlib.rcParams, 'backend')
if isinstance(val, str):
return val
return None
@isolated.decorate
def _list_available_mpl_backends():
"""
Returns the names of all available matplotlib backends.
"""
import matplotlib
return matplotlib.rcsetup.all_backends
@isolated.decorate
def _check_mpl_backend_importable(module_name):
"""
Attempts to import the given module name (matplotlib backend module).
Exceptions are propagated to caller.
"""
__import__(module_name)
# Bytecode scanning
def _recursive_scan_code_objects_for_mpl_use(co):
"""
Recursively scan the bytecode for occurrences of matplotlib.use() or mpl.use() calls with const arguments, and
collect those arguments into list of used matplotlib backend names.
"""
from PyInstaller.depend.bytecode import any_alias, recursive_function_calls
mpl_use_names = {
*any_alias("matplotlib.use"),
*any_alias("mpl.use"), # matplotlib is commonly aliased as mpl
}
backends = []
for calls in recursive_function_calls(co).values():
for name, args in calls:
# matplotlib.use(backend) or matplotlib.use(backend, force)
# We support only literal arguments. Similarly, kwargs are
# not supported.
if len(args) not in {1, 2} or not isinstance(args[0], str):
continue
if name in mpl_use_names:
backends.append(args[0])
return backends
def _backend_module_name(name):
"""
Converts matplotlib backend name to its corresponding module name.
Equivalent to matplotlib.cbook._backend_module_name().
"""
if name.startswith("module://"):
return name[9:]
return f"matplotlib.backends.backend_{name.lower()}"
def _autodetect_used_backends(hook_api):
"""
Returns a list of automatically-discovered matplotlib backends in use, or the name of the default matplotlib
backend. Implements the 'auto' backend selection method.
"""
# Scan the code for matplotlib.use()
modulegraph = hook_api.analysis.graph
mpl_code_objs = modulegraph.get_code_using("matplotlib")
used_backends = []
for name, co in mpl_code_objs.items():
co_backends = _recursive_scan_code_objects_for_mpl_use(co)
if co_backends:
logger.info(
"Discovered Matplotlib backend(s) via `matplotlib.use()` call in module %r: %r", name, co_backends
)
used_backends += co_backends
# Deduplicate and sort the list of used backends before displaying it.
used_backends = sorted(set(used_backends))
if used_backends:
HOOK_CONFIG_DOCS = 'https://pyinstaller.org/en/stable/hooks-config.html#matplotlib-hooks'
logger.info(
"The following Matplotlib backends were discovered by scanning for `matplotlib.use()` calls: %r. If your "
"backend of choice is not in this list, either add a `matplotlib.use()` call to your code, or configure "
"the backend collection via hook options (see: %s).", used_backends, HOOK_CONFIG_DOCS
)
return used_backends
# Determine the default matplotlib backend.
#
# Ideally, this would be done by calling ``matplotlib.get_backend()``. However, that function tries to switch to the
# default backend (calling ``matplotlib.pyplot.switch_backend()``), which seems to occasionally fail on our linux CI
# with an error and, on other occasions, returns the headless Agg backend instead of the GUI one (even with display
# server running). Furthermore, using ``matplotlib.get_backend()`` returns headless 'Agg' when display server is
# unavailable, which is not ideal for automated builds.
#
# Therefore, we try to emulate ``matplotlib.get_backend()`` ourselves. First, we try to obtain the configured
# default backend from settings (rcparams and/or MPLBACKEND environment variable). If that is unavailable, we try to
# find the first importable GUI-based backend, using the same list as matplotlib.pyplot.switch_backend() uses for
# automatic backend selection. The difference is that we only test whether the backend module is importable, without
# trying to switch to it.
default_backend = _get_configured_default_backend() # isolated sub-process
if default_backend:
logger.info("Found configured default matplotlib backend: %s", default_backend)
return [default_backend]
# `QtAgg` supersedes `Qt5Agg`; however, we keep `Qt5Agg` in the candidate list to support older versions of
# matplotlib that do not have `QtAgg`.
candidates = ["QtAgg", "Qt5Agg", "Gtk4Agg", "Gtk3Agg", "TkAgg", "WxAgg"]
if is_darwin:
candidates = ["MacOSX"] + candidates
logger.info("Trying determine the default backend as first importable candidate from the list: %r", candidates)
for candidate in candidates:
try:
module_name = _backend_module_name(candidate)
_check_mpl_backend_importable(module_name) # NOTE: uses an isolated sub-process.
except Exception:
continue
return [candidate]
# Fall back to headless Agg backend
logger.info("None of the backend candidates could be imported; falling back to headless Agg!")
return ['Agg']
def _collect_all_importable_backends(hook_api):
"""
Returns a list of all importable matplotlib backends. Implements the 'all' backend selection method.
"""
# List of the human-readable names of all available backends.
backend_names = _list_available_mpl_backends() # NOTE: retrieved in an isolated sub-process.
logger.info("All available matplotlib backends: %r", backend_names)
# Try to import the module(s).
importable_backends = []
# List of backends to exclude; Qt4 is not supported by PyInstaller anymore.
exclude_backends = {'Qt4Agg', 'Qt4Cairo'}
# Ignore "CocoaAgg" on OSes other than macOS; attempting to import it on other OSes halts the current
# (sub)process without printing output or raising exceptions, preventing reliable detection. Apply the
# same logic for the (newer) "MacOSX" backend.
if not is_darwin:
exclude_backends |= {'CocoaAgg', 'MacOSX'}
# For safety, attempt to import each backend in an isolated sub-process.
for backend_name in backend_names:
if backend_name in exclude_backends:
logger.info(' Matplotlib backend %r: excluded', backend_name)
continue
try:
module_name = _backend_module_name(backend_name)
_check_mpl_backend_importable(module_name) # NOTE: uses an isolated sub-process.
except Exception:
# Backend is not importable, for whatever reason.
logger.info(' Matplotlib backend %r: ignored due to import error', backend_name)
continue
logger.info(' Matplotlib backend %r: added', backend_name)
importable_backends.append(backend_name)
return importable_backends
def hook(hook_api):
# Backend collection setting
backends_method = get_hook_config(hook_api, 'matplotlib', 'backends')
if backends_method is None:
backends_method = 'auto' # default method
# Select backend(s)
if backends_method == 'auto':
logger.info("Matplotlib backend selection method: automatic discovery of used backends")
backend_names = _autodetect_used_backends(hook_api)
elif backends_method == 'all':
logger.info("Matplotlib backend selection method: collection of all importable backends")
backend_names = _collect_all_importable_backends(hook_api)
else:
logger.info("Matplotlib backend selection method: user-provided name(s)")
if isinstance(backends_method, str):
backend_names = [backends_method]
else:
assert isinstance(backends_method, list), "User-provided backend name(s) must be either a string or a list!"
backend_names = backends_method
# Deduplicate and sort the list of selected backends before displaying it.
backend_names = sorted(set(backend_names))
logger.info("Selected matplotlib backends: %r", backend_names)
# Set module names as hiddenimports
module_names = [_backend_module_name(backend) for backend in backend_names] # backend name -> module name
hook_api.add_imports(*module_names)
|