From 59522d0c5eb5a0edbd8f108210b8c1d624eb8002 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 14 Oct 2021 14:14:07 +0200 Subject: [PATCH] Raise an exception when find_tex_file fails to find a file. The exception message is clearer for end users than downstream callers failing to `open()` a file named `""`. Also update the function's docstring. _tfmfile now never returns None (an exception would have been raised earlier by find_tex_file), so remove the corresponding branch. --- .../deprecations/21356-AL.rst | 5 + lib/matplotlib/backends/backend_pdf.py | 2 +- lib/matplotlib/dviread.py | 116 +++++++++++------- lib/matplotlib/testing/__init__.py | 6 +- lib/matplotlib/tests/test_dviread.py | 2 +- lib/matplotlib/textpath.py | 2 +- 6 files changed, 82 insertions(+), 51 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/21356-AL.rst diff --git a/doc/api/next_api_changes/deprecations/21356-AL.rst b/doc/api/next_api_changes/deprecations/21356-AL.rst new file mode 100644 index 000000000000..de6cb79dd908 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/21356-AL.rst @@ -0,0 +1,5 @@ +In the future, ``dviread.find_tex_file`` will raise a ``FileNotFoundError`` for missing files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Previously, it would return an empty string in such cases. Raising an +exception allows attaching a user-friendly message instead. During the +transition period, a warning is raised. diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index bd0c370f1cba..29838c35316c 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -887,7 +887,7 @@ def dviFontName(self, dvifont): if dvi_info is not None: return dvi_info.pdfname - tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map')) + tex_font_map = dviread.PsfontsMap(dviread._find_tex_file('pdftex.map')) psfont = tex_font_map[dvifont.texname] if psfont.filename is None: raise ValueError( diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 3207a01de8be..7f90a13f1086 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -470,13 +470,12 @@ def _fnt_def_real(self, k, c, s, d, a, l): n = self.file.read(a + l) fontname = n[-l:].decode('ascii') tfm = _tfmfile(fontname) - if tfm is None: - raise FileNotFoundError("missing font metrics file: %s" % fontname) if c != 0 and tfm.checksum != 0 and c != tfm.checksum: raise ValueError('tfm checksum mismatch: %s' % n) - - vf = _vffile(fontname) - + try: + vf = _vffile(fontname) + except FileNotFoundError: + vf = None self.fonts[k] = DviFont(scale=s, tfm=tfm, texname=n, vf=vf) @_dispatch(247, state=_dvistate.pre, args=('u1', 'u4', 'u4', 'u4', 'u1')) @@ -938,9 +937,9 @@ def _parse_and_cache_line(self, line): if basename is None: basename = tfmname if encodingfile is not None: - encodingfile = find_tex_file(encodingfile) + encodingfile = _find_tex_file(encodingfile) if fontfile is not None: - fontfile = find_tex_file(fontfile) + fontfile = _find_tex_file(fontfile) self._parsed[tfmname] = PsFont( texname=tfmname, psname=basename, effects=effects, encoding=encodingfile, filename=fontfile) @@ -992,21 +991,20 @@ def search(self, filename): self._proc.stdin.write(os.fsencode(filename) + b"\n") self._proc.stdin.flush() out = self._proc.stdout.readline().rstrip() - return "" if out == b"nil" else os.fsdecode(out) + return None if out == b"nil" else os.fsdecode(out) @lru_cache() @_api.delete_parameter("3.5", "format") -def find_tex_file(filename, format=None): +def _find_tex_file(filename, format=None): """ - Find a file in the texmf tree. + Find a file in the texmf tree using kpathsea_. - Calls :program:`kpsewhich` which is an interface to the kpathsea - library [1]_. Most existing TeX distributions on Unix-like systems use - kpathsea. It is also available as part of MikTeX, a popular - distribution on Windows. + The kpathsea library, provided by most existing TeX distributions, both + on Unix-like systems and on Windows (MikTeX), is invoked via a long-lived + luatex process if luatex is installed, or via kpsewhich otherwise. - *If the file is not found, an empty string is returned*. + .. _kpathsea: https://www.tug.org/kpathsea/ Parameters ---------- @@ -1016,10 +1014,10 @@ def find_tex_file(filename, format=None): Could be e.g. 'tfm' or 'vf' to limit the search to that type of files. Deprecated. - References - ---------- - .. [1] `Kpathsea documentation `_ - The library that :program:`kpsewhich` is part of. + Raises + ------ + FileNotFoundError + If the file is not found. """ # we expect these to always be ascii encoded, but use utf-8 @@ -1029,39 +1027,63 @@ def find_tex_file(filename, format=None): if isinstance(format, bytes): format = format.decode('utf-8', errors='replace') - if format is None: + try: + lk = _LuatexKpsewhich() + except FileNotFoundError: + lk = None # Fallback to directly calling kpsewhich, as below. + + if lk and format is None: + path = lk.search(filename) + + else: + if os.name == 'nt': + # On Windows only, kpathsea can use utf-8 for cmd args and output. + # The `command_line_encoding` environment variable is set to force + # it to always use utf-8 encoding. See Matplotlib issue #11848. + kwargs = {'env': {**os.environ, 'command_line_encoding': 'utf-8'}, + 'encoding': 'utf-8'} + else: # On POSIX, run through the equivalent of os.fsdecode(). + kwargs = {'encoding': sys.getfilesystemencoding(), + 'errors': 'surrogateescape'} + + cmd = ['kpsewhich'] + if format is not None: + cmd += ['--format=' + format] + cmd += [filename] try: - lk = _LuatexKpsewhich() - except FileNotFoundError: - pass # Fallback to directly calling kpsewhich, as below. - else: - return lk.search(filename) - - if os.name == 'nt': - # On Windows only, kpathsea can use utf-8 for cmd args and output. - # The `command_line_encoding` environment variable is set to force it - # to always use utf-8 encoding. See Matplotlib issue #11848. - kwargs = {'env': {**os.environ, 'command_line_encoding': 'utf-8'}, - 'encoding': 'utf-8'} - else: # On POSIX, run through the equivalent of os.fsdecode(). - kwargs = {'encoding': sys.getfilesystemencoding(), - 'errors': 'surrogatescape'} - - cmd = ['kpsewhich'] - if format is not None: - cmd += ['--format=' + format] - cmd += [filename] + path = (cbook._check_and_log_subprocess(cmd, _log, **kwargs) + .rstrip('\n')) + except (FileNotFoundError, RuntimeError): + path = None + + if path: + return path + else: + raise FileNotFoundError( + f"Matplotlib's TeX implementation searched for a file named " + f"{filename!r} in your texmf tree, but could not find it") + + +# After the deprecation period elapses, delete this shim and rename +# _find_tex_file to find_tex_file everywhere. +@_api.delete_parameter("3.5", "format") +def find_tex_file(filename, format=None): try: - result = cbook._check_and_log_subprocess(cmd, _log, **kwargs) - except (FileNotFoundError, RuntimeError): - return '' - return result.rstrip('\n') + return (_find_tex_file(filename, format) if format is not None else + _find_tex_file(filename)) + except FileNotFoundError as exc: + _api.warn_deprecated( + "3.6", message=f"{exc.args[0]}; in the future, this will raise a " + f"FileNotFoundError.") + return "" + + +find_tex_file.__doc__ = _find_tex_file.__doc__ @lru_cache() def _fontfile(cls, suffix, texname): - filename = find_tex_file(texname + suffix) - return cls(filename) if filename else None + return cls(_find_tex_file(texname + suffix)) _tfmfile = partial(_fontfile, Tfm, ".tfm") @@ -1077,7 +1099,7 @@ def _fontfile(cls, suffix, texname): parser.add_argument("dpi", nargs="?", type=float, default=None) args = parser.parse_args() with Dvi(args.filename, args.dpi) as dvi: - fontmap = PsfontsMap(find_tex_file('pdftex.map')) + fontmap = PsfontsMap(_find_tex_file('pdftex.map')) for page in dvi: print(f"=== new page === " f"(w: {page.width}, h: {page.height}, d: {page.descent})") diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index f9c547ce00aa..754277c41f43 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -78,4 +78,8 @@ def _check_for_pgf(texsystem): def _has_tex_package(package): - return bool(mpl.dviread.find_tex_file(f"{package}.sty")) + try: + mpl.dviread._find_tex_file(f"{package}.sty") + return True + except FileNotFoundError: + return False diff --git a/lib/matplotlib/tests/test_dviread.py b/lib/matplotlib/tests/test_dviread.py index a40151fd555f..7e10975f44d5 100644 --- a/lib/matplotlib/tests/test_dviread.py +++ b/lib/matplotlib/tests/test_dviread.py @@ -7,7 +7,7 @@ def test_PsfontsMap(monkeypatch): - monkeypatch.setattr(dr, 'find_tex_file', lambda x: x) + monkeypatch.setattr(dr, '_find_tex_file', lambda x: x) filename = str(Path(__file__).parent / 'baseline_images/dviread/test.map') fontmap = dr.PsfontsMap(filename) diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index 9b14e79ec2d2..5ef56e4be885 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -279,7 +279,7 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, @staticmethod @functools.lru_cache(50) def _get_ps_font_and_encoding(texname): - tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map')) + tex_font_map = dviread.PsfontsMap(dviread._find_tex_file('pdftex.map')) psfont = tex_font_map[texname] if psfont.filename is None: raise ValueError(