Skip to content
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

Unify querying of executable versions #9639

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
248 changes: 134 additions & 114 deletions lib/matplotlib/__init__.py
Expand Up @@ -117,9 +117,9 @@
""")

import atexit
from collections import MutableMapping
from collections import MutableMapping, namedtuple
import contextlib
import distutils.version
from distutils.version import LooseVersion
import functools
import io
import importlib
Expand Down Expand Up @@ -184,9 +184,7 @@ def compare_versions(a, b):
"3.0", "compare_version 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 Down Expand Up @@ -424,138 +422,163 @@ def wrapper(*args, **kwargs):
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", "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); ``None`` if the executable is not found or
older that the oldest version supported by Matplotlib.
"""

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, use
# the *first group* of the match as the version.
# If min_ver is not None, emit a warning if the version is less than
# min_ver.
try:
proc = subprocess.Popen(
args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
universal_newlines=True)
proc.wait()
except OSError:
return None
match = re.search(regex, proc.stdout.read())
if match:
version = LooseVersion(match.group(1))
if min_ver is not None and version < min_ver:
warnings.warn("You have {} version {} but the minimum version "
"supported by Matplotlib is {}."
.format(args[0], version, min_ver))
return None
return _ExecInfo(args[0], version)
else:
return None

if name == "dvipng":
info = impl(["dvipng", "-version"], "(?m)^dvipng .* (.+)", "1.6")
elif name == "gs":
execs = (["gswin32c", "gswin64c", "mgs", "gs"] # "mgs" for miktex.
if sys.platform == "win32" else
["gs"])
info = next((info for info in (impl([e, "--version"], "(.*)", "9")
for e in execs)
if info),
None)
elif name == "inkscape":
info = impl(["inkscape", "-V"], "^Inkscape ([^ ]*)")
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"):
warnings.warn(
"You have pdftops version {} but the minimum version "
"supported by Matplotlib is 3.0.".format(info.version))
return None
else:
raise ValueError("Unknown executable: {!r}".format(name))
return info


def get_all_executable_infos():
"""
Get the version of some executables that Matplotlib optionally depends on.

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

Returns
-------
A mapping of the required executable to its corresponding information,
as returned by `get_executable_info`. The keys in the mapping are subject
to change without notice.
"""
return {name: get_executable_info(name)
for name in ["dvipng", "gs", "inkscape", "pdftops"]}


@cbook.deprecated("3.0")
def checkdep_dvipng():
try:
s = subprocess.Popen([str('dvipng'), '-version'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = s.communicate()
line = stdout.decode('ascii').split('\n')[1]
v = line.split()[-1]
return v
except (IndexError, ValueError, OSError):
return None
info = get_executable_info("dvipng")
return str(info.version) if info else None


@cbook.deprecated("3.0")
def checkdep_ghostscript():
if checkdep_ghostscript.executable is None:
if sys.platform == 'win32':
# mgs is the name in miktex
gs_execs = ['gswin32c', 'gswin64c', 'mgs', 'gs']
else:
gs_execs = ['gs']
for gs_exec in gs_execs:
try:
s = subprocess.Popen(
[gs_exec, '--version'], stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = s.communicate()
if s.returncode == 0:
v = stdout[:-1].decode('ascii')
checkdep_ghostscript.executable = gs_exec
checkdep_ghostscript.version = v
except (IndexError, ValueError, OSError):
pass
info = get_executable_info("gs")
if info:
checkdep_ghostscript.executable = info.executable
checkdep_ghostscript.version = str(info.version)
return checkdep_ghostscript.executable, checkdep_ghostscript.version
checkdep_ghostscript.executable = None
checkdep_ghostscript.version = None


@cbook.deprecated("3.0")
def checkdep_pdftops():
try:
s = subprocess.Popen([str('pdftops'), '-v'], stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = s.communicate()
lines = stderr.decode('ascii').split('\n')
for line in lines:
if 'version' in line:
v = line.split()[-1]
return v
except (IndexError, ValueError, UnboundLocalError, OSError):
return None
info = get_executable_info("pdftops")
return str(info.version) if info else None


@cbook.deprecated("3.0")
def checkdep_inkscape():
if checkdep_inkscape.version is None:
try:
s = subprocess.Popen([str('inkscape'), '-V'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = s.communicate()
lines = stdout.decode('ascii').split('\n')
for line in lines:
if 'Inkscape' in line:
v = line.split()[1]
break
checkdep_inkscape.version = v
except (IndexError, ValueError, UnboundLocalError, OSError):
pass
info = get_executable_info("inkscape")
if info:
checkdep_inkscape.version = str(info.version)
return checkdep_inkscape.version
checkdep_inkscape.version = None
Copy link
Member

Choose a reason for hiding this comment

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

That's just a caching mechanism, which is obsolete because get_executable_info() ls lru_cached.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I know, but it doesn't cost me anything to keep it around until checkdep_inkscape itself gets removed.



@cbook.deprecated("3.0")
def checkdep_ps_distiller(s):
if not s:
return False

flag = True
gs_req = '8.60'
gs_exec, gs_v = checkdep_ghostscript()
if not compare_versions(gs_v, gs_req):
flag = False
warnings.warn(('matplotlibrc ps.usedistiller option can not be used '
'unless ghostscript-%s or later is installed on your '
'system') % gs_req)

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
warnings.warn(('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:
if not get_executable_info("gs"):
warnings.warn(
"Setting matplotlibrc ps.usedistiller requires ghostscript.")
return False
if s == "xpdf" and not get_executable_info("pdftops"):
warnings.warn(
"Setting matplotlibrc ps.usedistiller to 'xpdf' requires xpdf.")
return False
return s


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

gs_req = '8.60'
dvipng_req = '1.6'
flag = True

if shutil.which("tex") is None:
flag = False
warnings.warn('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
warnings.warn('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
warnings.warn('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"):
warnings.warn("Setting matplotlibrc text.usetex requires TeX.")
return False
if not get_executable_info("dvipng"):
warnings.warn("Setting matplotlibrc text.usetex requires dvipng.")
return False
if not get_executable_info("gs"):
warnings.warn(
"Setting matplotlibrc text.usetex requires ghostscript.")
return False
return True


def _get_home():
Expand Down Expand Up @@ -1133,9 +1156,6 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True):
defaultParams.items()
if key not in _all_deprecated])

rcParams['ps.usedistiller'] = checkdep_ps_distiller(
rcParams['ps.usedistiller'])

rcParams['text.usetex'] = checkdep_usetex(rcParams['text.usetex'])

if rcParams['axes.formatter.use_locale']:
Expand Down
27 changes: 5 additions & 22 deletions lib/matplotlib/backends/backend_pgf.py
Expand Up @@ -146,41 +146,24 @@ 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:
if mpl.get_executable_info("gs"):
def gs_convert(pdffile, pngfile, dpi):
cmd = [gs,
cmd = [mpl.get_executable_info("gs").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