Skip to content

Commit 9506878

Browse files
committed
Unify querying of executable versions.
1 parent 9b48fd8 commit 9506878

File tree

2 files changed

+132
-135
lines changed

2 files changed

+132
-135
lines changed

lib/matplotlib/__init__.py

Lines changed: 129 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@
104104
from __future__ import absolute_import, division, print_function
105105

106106
import six
107+
from six.moves.urllib.request import urlopen
108+
from six.moves import reload_module as reload
107109

108110
import sys
109111
if sys.version_info < (3, 5): # noqa: E402
@@ -118,10 +120,9 @@
118120
""")
119121

120122
import atexit
121-
from collections import MutableMapping
123+
from collections import MutableMapping, namedtuple
122124
import contextlib
123-
import distutils.version
124-
import distutils.sysconfig
125+
from distutils.version import LooseVersion
125126
import functools
126127
import io
127128
import inspect
@@ -140,13 +141,11 @@
140141
# definitions, so it is safe to import from it here.
141142
from . import cbook
142143
from matplotlib.cbook import (
143-
_backports, mplDeprecation, dedent, get_label, sanitize_sequence)
144+
mplDeprecation, dedent, get_label, sanitize_sequence)
144145
from matplotlib.compat import subprocess
145146
from matplotlib.rcsetup import defaultParams, validate_backend, cycler
146147

147148
import numpy
148-
from six.moves.urllib.request import urlopen
149-
from six.moves import reload_module as reload
150149

151150
# Get the version from the _version.py versioneer file. For a git checkout,
152151
# this is computed based on the number of commits since the last tag.
@@ -191,9 +190,7 @@ def compare_versions(a, b):
191190
a = a.decode('ascii')
192191
if isinstance(b, bytes):
193192
b = b.decode('ascii')
194-
a = distutils.version.LooseVersion(a)
195-
b = distutils.version.LooseVersion(b)
196-
return a >= b
193+
return LooseVersion(a) >= LooseVersion(b)
197194
else:
198195
return False
199196

@@ -421,89 +418,123 @@ def wrapper(*args, **kwargs):
421418
return wrapper
422419

423420

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

436520

437521
def checkdep_ghostscript():
438-
if checkdep_ghostscript.executable is None:
439-
if sys.platform == 'win32':
440-
# mgs is the name in miktex
441-
gs_execs = ['gswin32c', 'gswin64c', 'mgs', 'gs']
442-
else:
443-
gs_execs = ['gs']
444-
for gs_exec in gs_execs:
445-
try:
446-
s = subprocess.Popen(
447-
[str(gs_exec), '--version'], stdout=subprocess.PIPE,
448-
stderr=subprocess.PIPE)
449-
stdout, stderr = s.communicate()
450-
if s.returncode == 0:
451-
v = stdout[:-1].decode('ascii')
452-
checkdep_ghostscript.executable = gs_exec
453-
checkdep_ghostscript.version = v
454-
except (IndexError, ValueError, OSError):
455-
pass
522+
info = get_executable_info("gs")
523+
checkdep_ghostscript.executable = info.executable
524+
checkdep_ghostscript.version = str(info.version)
456525
return checkdep_ghostscript.executable, checkdep_ghostscript.version
457526
checkdep_ghostscript.executable = None
458527
checkdep_ghostscript.version = None
459528

460529

461-
# Deprecated, as it is unneeded and some distributions (e.g. MiKTeX 2.9.6350)
462-
# do not actually report the TeX version.
463-
@cbook.deprecated("2.1")
464-
def checkdep_tex():
465-
try:
466-
s = subprocess.Popen([str('tex'), '-version'], stdout=subprocess.PIPE,
467-
stderr=subprocess.PIPE)
468-
stdout, stderr = s.communicate()
469-
line = stdout.decode('ascii').split('\n')[0]
470-
pattern = r'3\.1\d+'
471-
match = re.search(pattern, line)
472-
v = match.group(0)
473-
return v
474-
except (IndexError, ValueError, AttributeError, OSError):
475-
return None
476-
477-
530+
@cbook.deprecated("3.0")
478531
def checkdep_pdftops():
479-
try:
480-
s = subprocess.Popen([str('pdftops'), '-v'], stdout=subprocess.PIPE,
481-
stderr=subprocess.PIPE)
482-
stdout, stderr = s.communicate()
483-
lines = stderr.decode('ascii').split('\n')
484-
for line in lines:
485-
if 'version' in line:
486-
v = line.split()[-1]
487-
return v
488-
except (IndexError, ValueError, UnboundLocalError, OSError):
489-
return None
532+
return str(get_executable_info("pdftops").version)
490533

491534

535+
@cbook.deprecated("3.0")
492536
def checkdep_inkscape():
493-
if checkdep_inkscape.version is None:
494-
try:
495-
s = subprocess.Popen([str('inkscape'), '-V'],
496-
stdout=subprocess.PIPE,
497-
stderr=subprocess.PIPE)
498-
stdout, stderr = s.communicate()
499-
lines = stdout.decode('ascii').split('\n')
500-
for line in lines:
501-
if 'Inkscape' in line:
502-
v = line.split()[1]
503-
break
504-
checkdep_inkscape.version = v
505-
except (IndexError, ValueError, UnboundLocalError, OSError):
506-
pass
537+
checkdep_inkscape.version = str(get_executable_info("inkscape").version)
507538
return checkdep_inkscape.version
508539
checkdep_inkscape.version = None
509540

@@ -528,65 +559,31 @@ def checkdep_xmllint():
528559
def checkdep_ps_distiller(s):
529560
if not s:
530561
return False
531-
532-
flag = True
533-
gs_req = '8.60'
534-
gs_exec, gs_v = checkdep_ghostscript()
535-
if not compare_versions(gs_v, gs_req):
536-
flag = False
537-
warnings.warn(('matplotlibrc ps.usedistiller option can not be used '
538-
'unless ghostscript-%s or later is installed on your '
539-
'system') % gs_req)
540-
541-
if s == 'xpdf':
542-
pdftops_req = '3.0'
543-
pdftops_req_alt = '0.9' # poppler version numbers, ugh
544-
pdftops_v = checkdep_pdftops()
545-
if compare_versions(pdftops_v, pdftops_req):
546-
pass
547-
elif (compare_versions(pdftops_v, pdftops_req_alt) and not
548-
compare_versions(pdftops_v, '1.0')):
549-
pass
550-
else:
551-
flag = False
552-
warnings.warn(('matplotlibrc ps.usedistiller can not be set to '
553-
'xpdf unless xpdf-%s or later is installed on '
554-
'your system') % pdftops_req)
555-
556-
if flag:
557-
return s
558-
else:
562+
if not get_executable_info("gs"):
563+
warnings.warn(
564+
"Setting matplotlibrc ps.usedistiller requires ghostscript.")
559565
return False
566+
if s == "xpdf" and not get_executable_info("pdftops"):
567+
warnings.warn(
568+
"setting matplotlibrc ps.usedistiller to 'xpdf' requires xpdf.")
569+
return False
570+
return s
560571

561572

562573
def checkdep_usetex(s):
563574
if not s:
564575
return False
565-
566-
gs_req = '8.60'
567-
dvipng_req = '1.6'
568-
flag = True
569-
570-
if shutil.which("tex") is None:
571-
flag = False
572-
warnings.warn('matplotlibrc text.usetex option can not be used unless '
573-
'TeX is installed on your system')
574-
575-
dvipng_v = checkdep_dvipng()
576-
if not compare_versions(dvipng_v, dvipng_req):
577-
flag = False
578-
warnings.warn('matplotlibrc text.usetex can not be used with *Agg '
579-
'backend unless dvipng-%s or later is installed on '
580-
'your system' % dvipng_req)
581-
582-
gs_exec, gs_v = checkdep_ghostscript()
583-
if not compare_versions(gs_v, gs_req):
584-
flag = False
585-
warnings.warn('matplotlibrc text.usetex can not be used unless '
586-
'ghostscript-%s or later is installed on your system'
587-
% gs_req)
588-
589-
return flag
576+
if not get_executable_info("tex"):
577+
warnings.warn("Setting matplotlibrc text.usetex requires TeX.")
578+
return False
579+
if not get_executable_info("dvipng"):
580+
warnings.warn("Setting matplotlibrc text.usetex requires dvipng.")
581+
return False
582+
if not get_executable_info("gs"):
583+
warnings.warn(
584+
"Setting matplotlibrc text.usetex requires ghostscript.")
585+
return False
586+
return True
590587

591588

592589
def _get_home():

lib/matplotlib/testing/compare.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ def get_file_hash(path, block_size=2 ** 20):
108108
from matplotlib import checkdep_ghostscript
109109
md5.update(checkdep_ghostscript()[1].encode('utf-8'))
110110
elif path.endswith('.svg'):
111-
from matplotlib import checkdep_inkscape
112-
md5.update(checkdep_inkscape().encode('utf-8'))
111+
md5.update(str(matplotlib.get_executable_info("inkscape").version)
112+
.encode('utf-8'))
113113

114114
return md5.hexdigest()
115115

@@ -245,7 +245,7 @@ def cmd(old, new):
245245
converter['pdf'] = make_external_conversion_command(cmd)
246246
converter['eps'] = make_external_conversion_command(cmd)
247247

248-
if matplotlib.checkdep_inkscape() is not None:
248+
if matplotlib.get_executable_info("inkscape"):
249249
converter['svg'] = _SVGConverter()
250250

251251

0 commit comments

Comments
 (0)