Skip to content

Unify checking of executable info. #13303

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

Merged
merged 1 commit into from
Feb 23, 2019
Merged
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
5 changes: 5 additions & 0 deletions doc/api/next_api_changes/2018-01-27-AL.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Deprecations
````````````

``checkdep_dvipng``, ``checkdep_ghostscript``, ``checkdep_pdftops``, and
``checkdep_inkscape`` are deprecated.
202 changes: 145 additions & 57 deletions lib/matplotlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,10 @@
""")

import atexit
from collections import namedtuple
from collections.abc import MutableMapping
import contextlib
import distutils.version
from distutils.version import LooseVersion
import functools
import importlib
import inspect
Expand Down Expand Up @@ -179,9 +180,7 @@ def compare_versions(a, b):
"3.0", message="compare_versions arguments should be strs.")
b = b.decode('ascii')
if a:
a = distutils.version.LooseVersion(a)
b = distutils.version.LooseVersion(b)
return a >= b
return LooseVersion(a) >= LooseVersion(b)
else:
return False

Expand All @@ -195,7 +194,7 @@ def _check_versions():
("pyparsing", "2.0.1"),
]:
module = importlib.import_module(modname)
if distutils.version.LooseVersion(module.__version__) < minver:
if LooseVersion(module.__version__) < minver:
raise ImportError("Matplotlib requires {}>={}; you have {}"
.format(modname, minver, module.__version__))

Expand Down Expand Up @@ -282,6 +281,117 @@ def wrapper():
return wrapper


_ExecInfo = namedtuple("_ExecInfo", "executable version")


@functools.lru_cache()
def _get_executable_info(name):
"""
Get the version of some executable that Matplotlib optionally depends on.

.. warning:
The list of executables that this function supports is set according to
Matplotlib's internal needs, and may change without notice.

Parameters
----------
name : str
The executable to query. The following values are currently supported:
"dvipng", "gs", "inkscape", "magick", "pdftops". This list is subject
to change without notice.

Returns
-------
If the executable is found, a namedtuple with fields ``executable`` (`str`)
and ``version`` (`distutils.version.LooseVersion`, or ``None`` if the
version cannot be determined).

Raises
------
FileNotFoundError
If the executable is not found or older than the oldest version
supported by Matplotlib.
ValueError
If the executable is not one that we know how to query.
"""

def impl(args, regex, min_ver=None):
# Execute the subprocess specified by args; capture stdout and stderr.
# Search for a regex match in the output; if the match succeeds, the
# first group of the match is the version.
# Return an _ExecInfo if the executable exists, and has a version of
# at least min_ver (if set); else, raise FileNotFoundError.
output = subprocess.check_output(
args, stderr=subprocess.STDOUT, universal_newlines=True)
match = re.search(regex, output)
if match:
version = LooseVersion(match.group(1))
if min_ver is not None and version < min_ver:
raise FileNotFoundError(
f"You have {args[0]} version {version} but the minimum "
f"version supported by Matplotlib is {min_ver}.")
return _ExecInfo(args[0], version)
else:
raise FileNotFoundError(
f"Failed to determine the version of {args[0]} from "
f"{' '.join(args)}, which output {output}")

if name == "dvipng":
return impl(["dvipng", "-version"], "(?m)^dvipng .* (.+)", "1.6")
elif name == "gs":
execs = (["gswin32c", "gswin64c", "mgs", "gs"] # "mgs" for miktex.
if sys.platform == "win32" else
["gs"])
for e in execs:
try:
return impl([e, "--version"], "(.*)", "9")
except FileNotFoundError:
pass
raise FileNotFoundError("Failed to find a Ghostscript installation")
elif name == "inkscape":
return impl(["inkscape", "-V"], "^Inkscape ([^ ]*)")
elif name == "magick":
path = None
if sys.platform == "win32":
# Check the registry to avoid confusing ImageMagick's convert with
# Windows's builtin convert.exe.
import winreg
binpath = ""
for flag in [0, winreg.KEY_WOW64_32KEY, winreg.KEY_WOW64_64KEY]:
try:
with winreg.OpenKeyEx(
winreg.HKEY_LOCAL_MACHINE,
r"Software\Imagemagick\Current",
0, winreg.KEY_QUERY_VALUE | flag) as hkey:
binpath = winreg.QueryValueEx(hkey, "BinPath")[0]
except OSError:
pass
if binpath:
for name in ["convert.exe", "magick.exe"]:
candidate = Path(binpath, name)
if candidate.exists():
path = candidate
break
else:
path = "convert"
if path is None:
raise FileNotFoundError(
"Failed to find an ImageMagick installation")
return impl([path, "--version"], r"^Version: ImageMagick (\S*)")
elif name == "pdftops":
info = impl(["pdftops", "-v"], "^pdftops version (.*)")
if info and not ("3.0" <= info.version
# poppler version numbers.
or "0.9" <= info.version <= "1.0"):
raise FileNotFoundError(
f"You have pdftops version {info.version} but the minimum "
f"version supported by Matplotlib is 3.0.")
return info
else:
raise ValueError("Unknown executable: {!r}".format(name))


@cbook.deprecated("3.1")
def checkdep_dvipng():
try:
s = subprocess.Popen(['dvipng', '-version'],
Expand All @@ -295,6 +405,7 @@ def checkdep_dvipng():
return None


@cbook.deprecated("3.1")
def checkdep_ghostscript():
if checkdep_ghostscript.executable is None:
if sys.platform == 'win32':
Expand All @@ -320,6 +431,7 @@ def checkdep_ghostscript():
checkdep_ghostscript.version = None


@cbook.deprecated("3.1")
def checkdep_pdftops():
try:
s = subprocess.Popen(['pdftops', '-v'], stdout=subprocess.PIPE,
Expand All @@ -334,6 +446,7 @@ def checkdep_pdftops():
return None


@cbook.deprecated("3.1")
def checkdep_inkscape():
if checkdep_inkscape.version is None:
try:
Expand All @@ -356,64 +469,39 @@ def checkdep_inkscape():
def checkdep_ps_distiller(s):
if not s:
return False

flag = True
gs_exec, gs_v = checkdep_ghostscript()
if not gs_exec:
flag = False
_log.warning('matplotlibrc ps.usedistiller option can not be used '
'unless ghostscript 9.0 or later is installed on your '
'system.')

if s == 'xpdf':
pdftops_req = '3.0'
pdftops_req_alt = '0.9' # poppler version numbers, ugh
pdftops_v = checkdep_pdftops()
if compare_versions(pdftops_v, pdftops_req):
pass
elif (compare_versions(pdftops_v, pdftops_req_alt) and not
compare_versions(pdftops_v, '1.0')):
pass
else:
flag = False
_log.warning('matplotlibrc ps.usedistiller can not be set to xpdf '
'unless xpdf-%s or later is installed on your '
'system.', pdftops_req)

if flag:
return s
else:
try:
_get_executable_info("gs")
except FileNotFoundError:
_log.warning(
"Setting rcParams['ps.usedistiller'] requires ghostscript.")
return False
if s == "xpdf":
try:
_get_executable_info("pdftops")
except FileNotFoundError:
_log.warning(
"Setting rcParams['ps.usedistiller'] to 'xpdf' requires xpdf.")
return False
return s


def checkdep_usetex(s):
if not s:
return False

gs_req = '9.00'
dvipng_req = '1.6'
flag = True

if shutil.which("tex") is None:
flag = False
_log.warning('matplotlibrc text.usetex option can not be used unless '
'TeX is installed on your system.')

dvipng_v = checkdep_dvipng()
if not compare_versions(dvipng_v, dvipng_req):
flag = False
_log.warning('matplotlibrc text.usetex can not be used with *Agg '
'backend unless dvipng-%s or later is installed on '
'your system.', dvipng_req)

gs_exec, gs_v = checkdep_ghostscript()
if not compare_versions(gs_v, gs_req):
flag = False
_log.warning('matplotlibrc text.usetex can not be used unless '
'ghostscript-%s or later is installed on your system.',
gs_req)

return flag
if not shutil.which("tex"):
_log.warning("usetex mode requires TeX.")
return False
try:
Copy link
Member

Choose a reason for hiding this comment

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

We should try all three (even if one fails) so users can know to go install all 3 if they need them, not going to install things one-at-a-time (this is a pet peeve of mine...).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But this is actually only used as a test marker (for non-test use, we don't run checkdep_usetex and instead just... do whatever is explained in #13285, which you already approved :)), so that doesn't really matter.

Copy link
Member

Choose a reason for hiding this comment

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

👍

_get_executable_info("dvipng")
except FileNotFoundError:
_log.warning("usetex mode requires dvipng.")
return False
try:
_get_executable_info("gs")
except FileNotFoundError:
_log.warning("usetex mode requires ghostscript.")
return False
return True


@_logged_cached('$HOME=%s')
Expand Down
31 changes: 10 additions & 21 deletions lib/matplotlib/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

import numpy as np

import matplotlib as mpl
from matplotlib._animation_data import (
DISPLAY_TEMPLATE, INCLUDED_FRAMES, JS_INCLUDE, STYLE_INCLUDE)
from matplotlib import cbook, rcParams, rcParamsDefault, rc_context
Expand Down Expand Up @@ -709,29 +710,17 @@ def output_args(self):
@classmethod
def bin_path(cls):
binpath = super().bin_path()
if sys.platform == 'win32' and binpath == 'convert':
# Check the registry to avoid confusing ImageMagick's convert with
# Windows's builtin convert.exe.
import winreg
binpath = ''
for flag in (0, winreg.KEY_WOW64_32KEY, winreg.KEY_WOW64_64KEY):
try:
with winreg.OpenKeyEx(
winreg.HKEY_LOCAL_MACHINE,
r'Software\Imagemagick\Current',
0, winreg.KEY_QUERY_VALUE | flag) as hkey:
parent = winreg.QueryValueEx(hkey, 'BinPath')[0]
except OSError:
pass
if binpath:
for exe in ('convert.exe', 'magick.exe'):
candidate = os.path.join(parent, exe)
if os.path.exists(candidate):
binpath = candidate
break
rcParams[cls.exec_key] = rcParamsDefault[cls.exec_key] = binpath
if binpath == 'convert':
binpath = mpl._get_executable_info('magick').executable
return binpath

@classmethod
def isAvailable(cls):
try:
return super().isAvailable()
except FileNotFoundError: # May be raised by get_executable_info.
return False


# Combine ImageMagick options with pipe-based writing
@writers.register('imagemagick')
Expand Down
31 changes: 9 additions & 22 deletions lib/matplotlib/backends/backend_pgf.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,41 +145,28 @@ def _font_properties_str(prop):


def make_pdf_to_png_converter():
"""
Returns a function that converts a pdf file to a png file.
"""

tools_available = []
# check for pdftocairo
try:
subprocess.check_output(["pdftocairo", "-v"], stderr=subprocess.STDOUT)
tools_available.append("pdftocairo")
except OSError:
pass
# check for ghostscript
gs, ver = mpl.checkdep_ghostscript()
if gs:
tools_available.append("gs")

# pick converter
if "pdftocairo" in tools_available:
"""Returns a function that converts a pdf file to a png file."""
if shutil.which("pdftocairo"):
def cairo_convert(pdffile, pngfile, dpi):
cmd = ["pdftocairo", "-singlefile", "-png", "-r", "%d" % dpi,
pdffile, os.path.splitext(pngfile)[0]]
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
return cairo_convert
elif "gs" in tools_available:
try:
gs_info = mpl._get_executable_info("gs")
except FileNotFoundError:
pass
else:
def gs_convert(pdffile, pngfile, dpi):
cmd = [gs,
cmd = [gs_info.executable,
'-dQUIET', '-dSAFER', '-dBATCH', '-dNOPAUSE', '-dNOPROMPT',
'-dUseCIEColor', '-dTextAlphaBits=4',
'-dGraphicsAlphaBits=4', '-dDOINTERPOLATE',
'-sDEVICE=png16m', '-sOutputFile=%s' % pngfile,
'-r%d' % dpi, pdffile]
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
return gs_convert
else:
raise RuntimeError("No suitable pdf to png renderer found.")
raise RuntimeError("No suitable pdf to png renderer found.")


class LatexError(Exception):
Expand Down
9 changes: 5 additions & 4 deletions lib/matplotlib/backends/backend_ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import numpy as np

import matplotlib as mpl
from matplotlib import (
cbook, _path, __version__, rcParams, checkdep_ghostscript)
from matplotlib.backend_bases import (
Expand Down Expand Up @@ -1362,11 +1363,11 @@ def gs_distill(tmpfile, eps=False, ptype='letter', bbox=None, rotated=False):
psfile = tmpfile + '.ps'
dpi = rcParams['ps.distiller.res']

gs_exe, gs_version = checkdep_ghostscript()
cbook._check_and_log_subprocess(
[gs_exe, "-dBATCH", "-dNOPAUSE", "-r%d" % dpi,
"-sDEVICE=ps2write", paper_option,
"-sOutputFile=%s" % psfile, tmpfile], _log)
[mpl._get_executable_info("gs").executable,
"-dBATCH", "-dNOPAUSE", "-r%d" % dpi, "-sDEVICE=ps2write",
paper_option, "-sOutputFile=%s" % psfile, tmpfile],
_log)

os.remove(tmpfile)
shutil.move(psfile, tmpfile)
Expand Down
Loading