Skip to content

Commit 9c842f1

Browse files
committed
Unify querying of executable versions.
1 parent 7f0632d commit 9c842f1

File tree

6 files changed

+174
-178
lines changed

6 files changed

+174
-178
lines changed

lib/matplotlib/__init__.py

Lines changed: 129 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,9 @@
117117
""")
118118

119119
import atexit
120-
from collections import MutableMapping
120+
from collections import MutableMapping, namedtuple
121121
import contextlib
122-
import distutils.version
122+
from distutils.version import LooseVersion
123123
import functools
124124
import io
125125
import importlib
@@ -184,9 +184,7 @@ def compare_versions(a, b):
184184
"3.0", "compare_version arguments should be strs.")
185185
b = b.decode('ascii')
186186
if a:
187-
a = distutils.version.LooseVersion(a)
188-
b = distutils.version.LooseVersion(b)
189-
return a >= b
187+
return LooseVersion(a) >= LooseVersion(b)
190188
else:
191189
return False
192190

@@ -424,138 +422,158 @@ def wrapper(*args, **kwargs):
424422
return wrapper
425423

426424

425+
_ExecInfo = namedtuple("_ExecInfo", "executable version")
426+
427+
428+
@functools.lru_cache()
429+
def get_executable_info(name):
430+
"""Get the version of some executables that Matplotlib depends on.
431+
432+
.. warning:
433+
The list of executables that this function supports is set according to
434+
Matplotlib's internal needs, and may change without notice.
435+
436+
Parameters
437+
----------
438+
name : str
439+
The executable to query. The following values are currently supported:
440+
"dvipng", "gs", "inkscape", "pdftops", "tex". This list is subject to
441+
change without notice.
442+
443+
Returns
444+
-------
445+
If the executable is found, a namedtuple with fields ``executable`` (`str`)
446+
and ``version`` (`distutils.version.LooseVersion`, or ``None`` if the
447+
version cannot be determined); ``None`` if the executable is not found.
448+
"""
449+
450+
def impl(args, regex, min_ver=None):
451+
# Execute the subprocess specified by args; capture stdout and stderr.
452+
# Search for a regex match in the output; if the match succeeds, use
453+
# the *first group* of the match as the version.
454+
# If min_ver is not None, emit a warning if the version is less than
455+
# min_ver.
456+
try:
457+
proc = subprocess.Popen(
458+
args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
459+
universal_newlines=True)
460+
proc.wait()
461+
except OSError:
462+
return None
463+
match = re.search(regex, proc.stdout.read())
464+
if match:
465+
version = LooseVersion(match.group(1))
466+
if min_ver is not None and version < min_ver:
467+
warnings.warn("You have {} version {} but the minimum version "
468+
"supported by Matplotlib is {}."
469+
.format(args[0], version, min_ver))
470+
return None
471+
return _ExecInfo(args[0], version)
472+
else:
473+
return None
474+
475+
if name == "dvipng":
476+
info = impl(["dvipng", "-version"], "(?m)^dvipng .* (.+)", "1.6")
477+
elif name == "gs":
478+
execs = (["gswin32c", "gswin64c", "mgs", "gs"] # "mgs" for miktex.
479+
if sys.platform == "win32" else
480+
["gs"])
481+
info = next((info for info in (impl([e, "--version"], "(.*)", "9")
482+
for e in execs)
483+
if info),
484+
None)
485+
elif name == "inkscape":
486+
info = impl(["inkscape", "-V"], "^Inkscape ([^ ]*)")
487+
elif name == "pdftops":
488+
info = impl(["pdftops", "-v"], "^pdftops version (.*)")
489+
if info and not ("3.0" <= info.version
490+
# poppler version numbers.
491+
or "0.9" <= info.version <= "1.0"):
492+
warnings.warn(
493+
"You have pdftops version {} but the minimum version "
494+
"supported by Matplotlib is 3.0.".format(info.version))
495+
return None
496+
elif name == "tex":
497+
info = (_ExecInfo("tex", None) if shutil.which("tex") is not None
498+
else None)
499+
else:
500+
raise ValueError("Unknown executable: {!r}".format(name))
501+
return info
502+
503+
504+
def get_all_executable_infos():
505+
"""Query all executables that Matplotlib may need.
506+
507+
.. warning:
508+
The list of executables that this function queries is set according to
509+
Matplotlib's internal needs, and may change without notice.
510+
511+
Returns
512+
-------
513+
A mapping of the required executable to its corresponding information,
514+
as returned by `get_executable_info`. The keys in the mapping are subject
515+
to change without notice.
516+
"""
517+
return {name: get_executable_info(name)
518+
for name in ["dvipng", "gs", "inkscape", "pdftops", "tex"]}
519+
520+
521+
@cbook.deprecated("3.0")
427522
def checkdep_dvipng():
428-
try:
429-
s = subprocess.Popen([str('dvipng'), '-version'],
430-
stdout=subprocess.PIPE,
431-
stderr=subprocess.PIPE)
432-
stdout, stderr = s.communicate()
433-
line = stdout.decode('ascii').split('\n')[1]
434-
v = line.split()[-1]
435-
return v
436-
except (IndexError, ValueError, OSError):
437-
return None
523+
return str(get_executable_info("dvipng").version)
438524

439525

526+
@cbook.deprecated("3.0")
440527
def checkdep_ghostscript():
441-
if checkdep_ghostscript.executable is None:
442-
if sys.platform == 'win32':
443-
# mgs is the name in miktex
444-
gs_execs = ['gswin32c', 'gswin64c', 'mgs', 'gs']
445-
else:
446-
gs_execs = ['gs']
447-
for gs_exec in gs_execs:
448-
try:
449-
s = subprocess.Popen(
450-
[gs_exec, '--version'], stdout=subprocess.PIPE,
451-
stderr=subprocess.PIPE)
452-
stdout, stderr = s.communicate()
453-
if s.returncode == 0:
454-
v = stdout[:-1].decode('ascii')
455-
checkdep_ghostscript.executable = gs_exec
456-
checkdep_ghostscript.version = v
457-
except (IndexError, ValueError, OSError):
458-
pass
528+
info = get_executable_info("gs")
529+
checkdep_ghostscript.executable = info.executable
530+
checkdep_ghostscript.version = str(info.version)
459531
return checkdep_ghostscript.executable, checkdep_ghostscript.version
460532
checkdep_ghostscript.executable = None
461533
checkdep_ghostscript.version = None
462534

463535

536+
@cbook.deprecated("3.0")
464537
def checkdep_pdftops():
465-
try:
466-
s = subprocess.Popen([str('pdftops'), '-v'], stdout=subprocess.PIPE,
467-
stderr=subprocess.PIPE)
468-
stdout, stderr = s.communicate()
469-
lines = stderr.decode('ascii').split('\n')
470-
for line in lines:
471-
if 'version' in line:
472-
v = line.split()[-1]
473-
return v
474-
except (IndexError, ValueError, UnboundLocalError, OSError):
475-
return None
538+
return str(get_executable_info("pdftops").version)
476539

477540

541+
@cbook.deprecated("3.0")
478542
def checkdep_inkscape():
479-
if checkdep_inkscape.version is None:
480-
try:
481-
s = subprocess.Popen([str('inkscape'), '-V'],
482-
stdout=subprocess.PIPE,
483-
stderr=subprocess.PIPE)
484-
stdout, stderr = s.communicate()
485-
lines = stdout.decode('ascii').split('\n')
486-
for line in lines:
487-
if 'Inkscape' in line:
488-
v = line.split()[1]
489-
break
490-
checkdep_inkscape.version = v
491-
except (IndexError, ValueError, UnboundLocalError, OSError):
492-
pass
543+
checkdep_inkscape.version = str(get_executable_info("inkscape").version)
493544
return checkdep_inkscape.version
494545
checkdep_inkscape.version = None
495546

496547

548+
@cbook.deprecated("3.0")
497549
def checkdep_ps_distiller(s):
498550
if not s:
499551
return False
500-
501-
flag = True
502-
gs_req = '8.60'
503-
gs_exec, gs_v = checkdep_ghostscript()
504-
if not compare_versions(gs_v, gs_req):
505-
flag = False
506-
warnings.warn(('matplotlibrc ps.usedistiller option can not be used '
507-
'unless ghostscript-%s or later is installed on your '
508-
'system') % gs_req)
509-
510-
if s == 'xpdf':
511-
pdftops_req = '3.0'
512-
pdftops_req_alt = '0.9' # poppler version numbers, ugh
513-
pdftops_v = checkdep_pdftops()
514-
if compare_versions(pdftops_v, pdftops_req):
515-
pass
516-
elif (compare_versions(pdftops_v, pdftops_req_alt) and not
517-
compare_versions(pdftops_v, '1.0')):
518-
pass
519-
else:
520-
flag = False
521-
warnings.warn(('matplotlibrc ps.usedistiller can not be set to '
522-
'xpdf unless xpdf-%s or later is installed on '
523-
'your system') % pdftops_req)
524-
525-
if flag:
526-
return s
527-
else:
552+
if not get_executable_info("gs"):
553+
warnings.warn(
554+
"Setting matplotlibrc ps.usedistiller requires ghostscript.")
555+
return False
556+
if s == "xpdf" and not get_executable_info("pdftops"):
557+
warnings.warn(
558+
"Setting matplotlibrc ps.usedistiller to 'xpdf' requires xpdf.")
528559
return False
560+
return s
529561

530562

531563
def checkdep_usetex(s):
532564
if not s:
533565
return False
534-
535-
gs_req = '8.60'
536-
dvipng_req = '1.6'
537-
flag = True
538-
539-
if shutil.which("tex") is None:
540-
flag = False
541-
warnings.warn('matplotlibrc text.usetex option can not be used unless '
542-
'TeX is installed on your system')
543-
544-
dvipng_v = checkdep_dvipng()
545-
if not compare_versions(dvipng_v, dvipng_req):
546-
flag = False
547-
warnings.warn('matplotlibrc text.usetex can not be used with *Agg '
548-
'backend unless dvipng-%s or later is installed on '
549-
'your system' % dvipng_req)
550-
551-
gs_exec, gs_v = checkdep_ghostscript()
552-
if not compare_versions(gs_v, gs_req):
553-
flag = False
554-
warnings.warn('matplotlibrc text.usetex can not be used unless '
555-
'ghostscript-%s or later is installed on your system'
556-
% gs_req)
557-
558-
return flag
566+
if not get_executable_info("tex"):
567+
warnings.warn("Setting matplotlibrc text.usetex requires TeX.")
568+
return False
569+
if not get_executable_info("dvipng"):
570+
warnings.warn("Setting matplotlibrc text.usetex requires dvipng.")
571+
return False
572+
if not get_executable_info("gs"):
573+
warnings.warn(
574+
"Setting matplotlibrc text.usetex requires ghostscript.")
575+
return False
576+
return True
559577

560578

561579
def _get_home():
@@ -1133,9 +1151,6 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True):
11331151
defaultParams.items()
11341152
if key not in _all_deprecated])
11351153

1136-
rcParams['ps.usedistiller'] = checkdep_ps_distiller(
1137-
rcParams['ps.usedistiller'])
1138-
11391154
rcParams['text.usetex'] = checkdep_usetex(rcParams['text.usetex'])
11401155

11411156
if rcParams['axes.formatter.use_locale']:

lib/matplotlib/backends/backend_pgf.py

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -184,41 +184,24 @@ def _font_properties_str(prop):
184184

185185

186186
def make_pdf_to_png_converter():
187-
"""
188-
Returns a function that converts a pdf file to a png file.
189-
"""
190-
191-
tools_available = []
192-
# check for pdftocairo
193-
try:
194-
subprocess.check_output(["pdftocairo", "-v"], stderr=subprocess.STDOUT)
195-
tools_available.append("pdftocairo")
196-
except OSError:
197-
pass
198-
# check for ghostscript
199-
gs, ver = mpl.checkdep_ghostscript()
200-
if gs:
201-
tools_available.append("gs")
202-
203-
# pick converter
204-
if "pdftocairo" in tools_available:
187+
"""Returns a function that converts a pdf file to a png file."""
188+
if shutil.which("pdftocairo"):
205189
def cairo_convert(pdffile, pngfile, dpi):
206190
cmd = ["pdftocairo", "-singlefile", "-png", "-r", "%d" % dpi,
207191
pdffile, os.path.splitext(pngfile)[0]]
208192
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
209193
return cairo_convert
210-
elif "gs" in tools_available:
194+
if mpl.get_executable_info("gs"):
211195
def gs_convert(pdffile, pngfile, dpi):
212-
cmd = [gs,
196+
cmd = [mpl.get_executable_info("gs").executable,
213197
'-dQUIET', '-dSAFER', '-dBATCH', '-dNOPAUSE', '-dNOPROMPT',
214198
'-dUseCIEColor', '-dTextAlphaBits=4',
215199
'-dGraphicsAlphaBits=4', '-dDOINTERPOLATE',
216200
'-sDEVICE=png16m', '-sOutputFile=%s' % pngfile,
217201
'-r%d' % dpi, pdffile]
218202
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
219203
return gs_convert
220-
else:
221-
raise RuntimeError("No suitable pdf to png renderer found.")
204+
raise RuntimeError("No suitable pdf to png renderer found")
222205

223206

224207
class LatexError(Exception):

0 commit comments

Comments
 (0)