8000 Unify querying of executable versions. · matplotlib/matplotlib@3c24671 · GitHub
[go: up one dir, main page]

Skip to content

Commit 3c24671

Browse files
committed
Unify querying of executable versions.
1 parent 4753f6c commit 3c24671

File tree

2 files changed

+139
-135
lines changed

2 files changed

+139
-135
lines changed

lib/matplotlib/__init__.py

Lines changed: 136 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,13 @@
102102
from __future__ import absolute_import, division, print_function
103103

104104
import six
105+
from six.moves.urllib.request import urlopen
106+
from six.moves import reload_module as reload
105107

106108
import atexit
107-
from collections import MutableMapping
109+
from collections import MutableMapping, namedtuple
108110
import contextlib
109-
import distutils.version
110-
import distutils.sysconfig
111+
from distutils.version import LooseVersion
111112
import functools
112113
import io
113114
import inspect
@@ -122,6 +123,11 @@
122123
import tempfile
123124
import warnings
124125

126+
try:
127+
from functools import lru_cache
128+
except ImportError:
129+
from backports.functools_lru_cache import lru_cache
130+
125131
# cbook must import matplotlib only within function
126132
# definitions, so it is safe to import from it here.
127133
from . import cbook
@@ -131,8 +137,6 @@
131137
from matplotlib.rcsetup import defaultParams, validate_backend, cycler
132138

133139
import numpy
134-
from six.moves.urllib.request import urlopen
135-
from six.moves import reload_module as reload
136140

137141
# Get the version from the _version.py versioneer file. For a git checkout,
138142
# this is computed based on the number of commits since the last tag.
@@ -177,9 +181,7 @@ def compare_versions(a, b):
177181
a = a.decode('ascii')
178182
if isinstance(b, bytes):
179183
b = b.decode('ascii')
180-
a = distutils.version.LooseVersion(a)
181-
b = distutils.version.LooseVersion(b)
182-
return a >= b
184+
return LooseVersion(a) >= LooseVersion(b)
183185
else:
184186
return False
185187

@@ -407,89 +409,125 @@ def wrapper(*args, **kwargs):
407409
return wrapper
408410

409411

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

422513

423514
def checkdep_ghostscript():
424-
if checkdep_ghostscript.executable is None:
425-
if sys.platform == 'win32':
426-
# mgs is the name in miktex
427-
gs_execs = ['gswin32c', 'gswin64c', 'mgs', 'gs']
428-
else:
429-
gs_execs = ['gs']
430-
for gs_exec in gs_execs:
431-
try:
432-
s = subprocess.Popen(
433-
[str(gs_exec), '--version'], stdout=subprocess.PIPE,
434-
stderr=subprocess.PIPE)
435-
stdout, stderr = s.communicate()
436-
if s.returncode == 0:
437-
v = stdout[:-1].decode('ascii')
438-
checkdep_ghostscript.executable = gs_exec
439-
checkdep_ghostscript.version = v
440-
except (IndexError, ValueError, OSError):
441-
pass
515+
info = get_executable_info("gs")
516+
checkdep_ghostscript.executable = info.executable
517+
checkdep_ghostscript.version = str(info.version)
442518
return checkdep_ghostscript.executable, checkdep_ghostscript.version
443519
checkdep_ghostscript.executable = None
444520
checkdep_ghostscript.version = None
445521

446522

447-
# Deprecated, as it is unneeded and some distributions (e.g. MiKTeX 2.9.6350)
448-
# do not actually report the TeX version.
449-
@cbook.deprecated("2.1")
450-
def checkdep_tex():
451-
try:
452-
s = subprocess.Popen([str('tex'), '-version'], stdout=subprocess.PIPE,
453-
stderr=subprocess.PIPE)
454-
stdout, stderr = s.communicate()
455-
line = stdout.decode('ascii').split('\n')[0]
456-
pattern = r'3\.1\d+'
457-
match = re.search(pattern, line)
458-
v = match.group(0)
459-
return v
460-
except (IndexError, ValueError, AttributeError, OSError):
461-
return None
462-
463-
523+
@cbook.deprecated("2.2")
464524
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
525+
return str(get_executable_info("pdftops").version)
476526

477527

528+
@cbook.deprecated("2.2")
478529
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
530+
checkdep_inkscape.version = str(get_executable_info("inkscape").version)
493531
return checkdep_inkscape.version
494532
checkdep_inkscape.version = None
495533

@@ -514,65 +552,31 @@ def checkdep_xmllint():
514552
def checkdep_ps_distiller(s):
515553
if not s:
516554
return False
517-
518-
flag = True
519-
gs_req = '8.60'
520-
gs_exec, gs_v = checkdep_ghostscript()
521-
if not compare_versions(gs_v, gs_req):
522-
flag = False
523-
warnings.warn(('matplotlibrc ps.usedistiller option can not be used '
524-
'unless ghostscript-%s or later is installed on your '
525-
'system') % gs_req)
526-
527-
if s == 'xpdf':
528-
pdftops_req = '3.0'
529-
pdftops_req_alt = '0.9' # poppler version numbers, ugh
530-
pdftops_v = checkdep_pdftops()
531-
if compare_versions(pdftops_v, pdftops_req):
532-
pass
533-
elif (compare_versions(pdftops_v, pdftops_req_alt) and not
534-
compare_versions(pdftops_v, '1.0')):
535-
pass
536-
else:
537-
flag = False
538-
warnings.warn(('matplotlibrc ps.usedistiller can not be set to '
539-
'xpdf unless xpdf-%s or later is installed on '
540-
'your system') % pdftops_req)
541-
542-
if flag:
543-
return s
544-
else:
555+
if not get_executable_info("gs"):
556+
warnings.warn(
557+
"Setting matplotlibrc ps.usedistiller requires ghostscript.")
558+
return False
559+
if s == "xpdf" and not get_executable_info("pdftops"):
560+
warnings.warn(
561+
"setting matplotlibrc ps.usedistiller to 'xpdf' requires xpdf.")
545562
return False
563+
return s
546564

547565

548566
def checkdep_usetex(s):
549567
if not s:
550568
return False
551-
552-
gs_req = '8.60'
553-
dvipng_req = '1.6'
554-
flag = True
555-
556-
if _backports.which("tex") is None:
557-
flag = False
558-
warnings.warn('matplotlibrc text.usetex option can not be used unless '
559-
'TeX is installed on your system')
560-
561-
dvipng_v = checkdep_dvipng()
562-
if not compare_versions(dvipng_v, dvipng_req):
563-
flag = False
564-
warnings.warn('matplotlibrc text.usetex can not be used with *Agg '
565-
'backend unless dvipng-%s or later is installed on '
566-
'your system' % dvipng_req)
567-
568-
gs_exec, gs_v = checkdep_ghostscript()
569-
if not compare_versions(gs_v, gs_req):
570-
flag = False
571-
warnings.warn('matplotlibrc text.usetex can not be used unless '
572-
'ghostscript-%s or later is installed on your system'
573-
% gs_req)
574-
575-
return flag
569+
if not get_executable_info("tex"):
570+
warnings.warn("Setting matplotlibrc text.usetex requires TeX.")
571+
return False
572+
if not get_executable_info("dvipng"):
573+
warnings.warn("Setting matplotlibrc text.usetex requires dvipng.")
574+
return False
575+
if not get_executable_info("gs"):
576+
warnings.warn(
577+
"Setting matplotlibrc text.usetex requires ghostscript.")
578+
return False
579+
return True
576580

577581

578582
def _get_home():
@@ -1361,7 +1365,7 @@ def use(arg, warn=True, force=False):
13611365
# Check if we've already set up a backend
13621366
if 'matplotlib.backends' in sys.modules:
13631367
# Warn only if called with a different name
1364-
if (rcParams['backend'] != name) and warn:
1368+
if (rcParams['backend'] != name) and warn:
13651369
import matplotlib.backends
13661370
warnings.warn(
13671371
_use_error_msg.format(

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)
0