SpaceLife-Updater/venv/lib64/python3.12/site-packages/PyInstaller/hooks/hook-matplotlib.backends.py

225 lines
9.3 KiB
Python
Executable File

#-----------------------------------------------------------------------------
# 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]
candidates = ["Qt5Agg", "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 Mac OS; 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)