diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 09856d9e1cfc..3aaa8c41cab4 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -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 @@ -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 @@ -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 +@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(): @@ -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']: diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 03b816f0a3ba..746f51dea1e3 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -146,32 +146,16 @@ 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', @@ -179,8 +163,7 @@ def gs_convert(pdffile, pngfile, dpi): '-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): diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index e02b37df7c5c..d0d0a6235a7a 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -1,6 +1,7 @@ """ -A PostScript backend, which can produce both PostScript .ps and .eps +A PostScript backend, which can produce both PostScript .ps and .eps. """ + import six from six.moves import StringIO @@ -10,15 +11,15 @@ import subprocess from tempfile import mkstemp + +import matplotlib as mpl from matplotlib import cbook, __version__, rcParams, checkdep_ghostscript from matplotlib.afm import AFM from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, RendererBase) - from matplotlib.cbook import (get_realpath_and_stat, is_writable_file_like, maxdict, file_requires_unicode) - from matplotlib.font_manager import findfont, is_opentype_cff_font, get_font from matplotlib.ft2font import KERNING_DEFAULT, LOAD_NO_HINTING from matplotlib.ttconv import convert_ttf_to_ps @@ -48,6 +49,7 @@ def __init__(self): self._cached = {} @property + @cbook.deprecated("3.0") def gs_exe(self): """ executable name of ghostscript. @@ -65,6 +67,7 @@ def gs_exe(self): return str(gs_exe) @property + @cbook.deprecated("3.0") def gs_version(self): """ version of ghostscript. @@ -90,6 +93,7 @@ def gs_version(self): return gs_version @property + @cbook.deprecated("3.0") def supports_ps2write(self): """ True if the installed ghostscript supports ps2write device. @@ -1494,14 +1498,9 @@ def gs_distill(tmpfile, eps=False, ptype='letter', bbox=None, rotated=False): psfile = tmpfile + '.ps' dpi = rcParams['ps.distiller.res'] - gs_exe = ps_backend_helper.gs_exe - if ps_backend_helper.supports_ps2write: # gs version >= 9 - device_name = "ps2write" - else: - device_name = "pswrite" - - command = [str(gs_exe), "-dBATCH", "-dNOPAUSE", "-r%d" % dpi, - "-sDEVICE=%s" % device_name, paper_option, + command = [mpl.get_executable_info("gs").executable, + "-dBATCH", "-dNOPAUSE", "-r%d" % dpi, + "-sDEVICE=ps2write", paper_option, "-sOutputFile=%s" % psfile, tmpfile] _log.debug(command) try: @@ -1524,11 +1523,7 @@ def gs_distill(tmpfile, eps=False, ptype='letter', bbox=None, rotated=False): # For some versions of gs, above steps result in an ps file # where the original bbox is no more correct. Do not adjust # bbox for now. - if ps_backend_helper.supports_ps2write: - # fo gs version >= 9 w/ ps2write device - pstoeps(tmpfile, bbox, rotated=rotated) - else: - pstoeps(tmpfile) + pstoeps(tmpfile, bbox, rotated=rotated) def xpdf_distill(tmpfile, eps=False, ptype='letter', bbox=None, rotated=False): @@ -1608,8 +1603,8 @@ def get_bbox(tmpfile, bbox): hack. """ - gs_exe = ps_backend_helper.gs_exe - command = [gs_exe, "-dBATCH", "-dNOPAUSE", "-sDEVICE=bbox", "%s" % tmpfile] + command = [get_executable_info("gs").executable, + "-dBATCH", "-dNOPAUSE", "-sDEVICE=bbox", "%s" % tmpfile] _log.debug(command) p = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -1622,11 +1617,12 @@ def get_bbox(tmpfile, bbox): if bbox_found: bbox_info = bbox_found.group() else: - raise RuntimeError('Ghostscript was not able to extract a bounding box.\ -Here is the Ghostscript output:\n\n%s' % bbox_info) + raise RuntimeError( + "Ghostscript was not able to extract a bounding box. " + "Here is the Ghostscript output:\n\n%s" % bbox_info) l, b, r, t = [float(i) for i in bbox_info.split()[-4:]] - # this is a hack to deal with the fact that ghostscript does not return the + # This is a hack to deal with the fact that ghostscript does not return the # intended bbox, but a tight bbox. For now, we just center the ink in the # intended bbox. This is not ideal, users may intend the ink to not be # centered. diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index d3ea5a7120cd..3c1515263552 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -13,6 +13,7 @@ parameter set listed here should also be visited to the :file:`matplotlibrc.template` in matplotlib's root source directory. """ + import six from collections import Iterable, Mapping @@ -22,6 +23,7 @@ import warnings import re +import matplotlib as mpl from matplotlib import cbook from matplotlib.cbook import mplDeprecation, deprecated, ls_mapper from matplotlib.fontconfig_pattern import parse_fontconfig_pattern @@ -507,7 +509,12 @@ def validate_ps_distiller(s): elif s in ('false', False): return False elif s in ('ghostscript', 'xpdf'): - return s + if not mpl.get_executable_info("gs"): + warnings.warn("Setting ps.usedistiller requires ghostscript.") + return False + if s == "xpdf" and not mpl.get_executable_info("pdftops"): + warnings.warn("Setting ps.usedistiller to 'xpdf' requires xpdf.") + return False else: raise ValueError('matplotlibrc ps.usedistiller must either be none, ' 'ghostscript or xpdf') diff --git a/lib/matplotlib/testing/compare.py b/lib/matplotlib/testing/compare.py index 0f5e149a567b..f4476e25a35c 100644 --- a/lib/matplotlib/testing/compare.py +++ b/lib/matplotlib/testing/compare.py @@ -102,11 +102,11 @@ def get_file_hash(path, block_size=2 ** 20): md5.update(data) if path.endswith('.pdf'): - from matplotlib import checkdep_ghostscript - md5.update(checkdep_ghostscript()[1].encode('utf-8')) + md5.update(str(matplotlib.get_executable_info("gs").version) + .encode('utf-8')) elif path.endswith('.svg'): - from matplotlib import checkdep_inkscape - md5.update(checkdep_inkscape().encode('utf-8')) + md5.update(str(matplotlib.get_executable_info("inkscape").version) + .encode('utf-8')) return md5.hexdigest() @@ -178,7 +178,7 @@ def __call__(self, orig, dest): if not self._proc: self._stdout = TemporaryFile() self._proc = subprocess.Popen( - [matplotlib.checkdep_ghostscript.executable, + [matplotlib.get_executable_info("gs").executable, "-dNOPAUSE", "-sDEVICE=png16m"], # As far as I can see, ghostscript never outputs to stderr. stdin=subprocess.PIPE, stdout=subprocess.PIPE) @@ -266,20 +266,15 @@ def __call__(self, orig, dest): sys.getfilesystemencoding(), "replace")) -def _update_converter(): - gs, gs_v = matplotlib.checkdep_ghostscript() - if gs_v is not None: - converter['pdf'] = converter['eps'] = _GSConverter() - if matplotlib.checkdep_inkscape() is not None: - converter['svg'] = _SVGConverter() - - -#: A dictionary that maps filename extensions to functions which -#: themselves map arguments `old` and `new` (filenames) to a list of strings. -#: The list can then be passed to Popen to convert files with that -#: extension to png format. -converter = {} -_update_converter() +#: A dictionary that maps filename extensions to functions which themselves map +#: arguments `old` and `new` (filenames) to a list of strings. The list can +#: then be passed to Popen to convert files with that extension to png format. +converter = { + **({"pdf": _GSConverter(), "eps": _GSConverter()} + if matplotlib.get_executable_info("gs") else {}), + **({"svg": _SVGConverter()} + if matplotlib.get_executable_info("inkscape") else {}), +} def comparable_formats(): diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index fd0d192c3e38..777c0ab180f9 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -16,7 +16,7 @@ needs_ghostscript = pytest.mark.xfail( - matplotlib.checkdep_ghostscript()[0] is None, + matplotlib.get_executable_info("gs") is None, reason="This test needs a ghostscript installation")