From 8f2e158edb6ee85e6c96fb4294490a1e01177c38 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 14 Aug 2025 23:09:26 +0200 Subject: [PATCH] Use pathlib in texmanager. Only a few changes are needed. Note that make_png returns an absolute path so path joining is not even needed here. Also restore the recently incorrectly removed usage of dpi in generating the png filename. Also drop an unrelated but outdated comment re: background handling, which is obsolete since 8940c66 (look for `if hack: ...`). --- lib/matplotlib/texmanager.py | 64 +++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/lib/matplotlib/texmanager.py b/lib/matplotlib/texmanager.py index 2202431b005d..feb72ca04ec0 100644 --- a/lib/matplotlib/texmanager.py +++ b/lib/matplotlib/texmanager.py @@ -23,7 +23,6 @@ import functools import hashlib import logging -import os from pathlib import Path import subprocess from tempfile import TemporaryDirectory @@ -63,7 +62,7 @@ class TexManager: Repeated calls to this constructor always return the same instance. """ - _texcache = os.path.join(mpl.get_cachedir(), 'tex.cache') + _cache_dir = Path(mpl.get_cachedir(), 'tex.cache') _grey_arrayd = {} _font_families = ('serif', 'sans-serif', 'cursive', 'monospace') @@ -109,7 +108,7 @@ class TexManager: @functools.lru_cache # Always return the same instance. def __new__(cls): - Path(cls._texcache).mkdir(parents=True, exist_ok=True) + cls._cache_dir.mkdir(parents=True, exist_ok=True) return object.__new__(cls) @classmethod @@ -167,23 +166,30 @@ def _get_font_preamble_and_command(cls): return preamble, fontcmd @classmethod - def get_basefile(cls, tex, fontsize, dpi=None): + def _get_base_path(cls, tex, fontsize, dpi=None): """ - Return a filename based on a hash of the string, fontsize, and dpi. + Return a file path based on a hash of the string, fontsize, and dpi. """ src = cls._get_tex_source(tex, fontsize) + str(dpi) filehash = hashlib.sha256( src.encode('utf-8'), usedforsecurity=False ).hexdigest() - filepath = Path(cls._texcache) + filepath = cls._cache_dir num_letters, num_levels = 2, 2 for i in range(0, num_letters*num_levels, num_letters): - filepath = filepath / Path(filehash[i:i+2]) + filepath = filepath / filehash[i:i+2] filepath.mkdir(parents=True, exist_ok=True) - return os.path.join(filepath, filehash) + return filepath / filehash + + @classmethod + def get_basefile(cls, tex, fontsize, dpi=None): # Kept for backcompat. + """ + Return a filename based on a hash of the string, fontsize, and dpi. + """ + return str(cls._get_base_path(tex, fontsize, dpi)) @classmethod def get_font_preamble(cls): @@ -243,17 +249,16 @@ def make_tex(cls, tex, fontsize): Return the file name. """ - texfile = cls.get_basefile(tex, fontsize) + ".tex" - Path(texfile).write_text(cls._get_tex_source(tex, fontsize), - encoding='utf-8') - return texfile + texpath = cls._get_base_path(tex, fontsize).with_suffix(".tex") + texpath.write_text(cls._get_tex_source(tex, fontsize), encoding='utf-8') + return str(texpath) @classmethod def _run_checked_subprocess(cls, command, tex, *, cwd=None): _log.debug(cbook._pformat_subprocess(command)) try: report = subprocess.check_output( - command, cwd=cwd if cwd is not None else cls._texcache, + command, cwd=cwd if cwd is not None else cls._cache_dir, stderr=subprocess.STDOUT) except FileNotFoundError as exc: raise RuntimeError( @@ -281,8 +286,8 @@ def make_dvi(cls, tex, fontsize): Return the file name. """ - dvifile = Path(cls.get_basefile(tex, fontsize)).with_suffix(".dvi") - if not dvifile.exists(): + dvipath = cls._get_base_path(tex, fontsize).with_suffix(".dvi") + if not dvipath.exists(): # Generate the tex and dvi in a temporary directory to avoid race # conditions e.g. if multiple processes try to process the same tex # string at the same time. Having tmpdir be a subdirectory of the @@ -292,17 +297,17 @@ def make_dvi(cls, tex, fontsize): # the absolute path may contain characters (e.g. ~) that TeX does # not support; n.b. relative paths cannot traverse parents, or it # will be blocked when `openin_any = p` in texmf.cnf). - with TemporaryDirectory(dir=dvifile.parent) as tmpdir: + with TemporaryDirectory(dir=dvipath.parent) as tmpdir: Path(tmpdir, "file.tex").write_text( cls._get_tex_source(tex, fontsize), encoding='utf-8') cls._run_checked_subprocess( ["latex", "-interaction=nonstopmode", "--halt-on-error", "file.tex"], tex, cwd=tmpdir) - Path(tmpdir, "file.dvi").replace(dvifile) + Path(tmpdir, "file.dvi").replace(dvipath) # Also move the tex source to the main cache directory, but # only for backcompat. - Path(tmpdir, "file.tex").replace(dvifile.with_suffix(".tex")) - return str(dvifile) + Path(tmpdir, "file.tex").replace(dvipath.with_suffix(".tex")) + return str(dvipath) @classmethod def make_png(cls, tex, fontsize, dpi): @@ -311,13 +316,12 @@ def make_png(cls, tex, fontsize, dpi): Return the file name. """ - pngfile = Path(cls.get_basefile(tex, fontsize)).with_suffix(".png") - # see get_rgba for a discussion of the background - if not pngfile.exists(): - dvifile = cls.make_dvi(tex, fontsize) - with TemporaryDirectory(dir=pngfile.parent) as tmpdir: + pngpath = cls._get_base_path(tex, fontsize, dpi).with_suffix(".png") + if not pngpath.exists(): + dvipath = cls.make_dvi(tex, fontsize) + with TemporaryDirectory(dir=pngpath.parent) as tmpdir: cmd = ["dvipng", "-bg", "Transparent", "-D", str(dpi), - "-T", "tight", "-o", "file.png", dvifile] + "-T", "tight", "-o", "file.png", dvipath] # When testing, disable FreeType rendering for reproducibility; # but dvipng 1.16 has a bug (fixed in f3ff241) that breaks # --freetype0 mode, so for it we keep FreeType enabled; the @@ -326,8 +330,8 @@ def make_png(cls, tex, fontsize, dpi): mpl._get_executable_info("dvipng").raw_version != "1.16"): cmd.insert(1, "--freetype0") cls._run_checked_subprocess(cmd, tex, cwd=tmpdir) - Path(tmpdir, "file.png").replace(pngfile) - return str(pngfile) + Path(tmpdir, "file.png").replace(pngpath) + return str(pngpath) @classmethod def get_grey(cls, tex, fontsize=None, dpi=None): @@ -338,7 +342,7 @@ def get_grey(cls, tex, fontsize=None, dpi=None): alpha = cls._grey_arrayd.get(key) if alpha is None: pngfile = cls.make_png(tex, fontsize, dpi) - rgba = mpl.image.imread(os.path.join(cls._texcache, pngfile)) + rgba = mpl.image.imread(pngfile) cls._grey_arrayd[key] = alpha = rgba[:, :, -1] return alpha @@ -364,9 +368,9 @@ def get_text_width_height_descent(cls, tex, fontsize, renderer=None): """Return width, height and descent of the text.""" if tex.strip() == '': return 0, 0, 0 - dvifile = cls.make_dvi(tex, fontsize) + dvipath = cls.make_dvi(tex, fontsize) dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1 - with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi: + with dviread.Dvi(dvipath, 72 * dpi_fraction) as dvi: page, = dvi # A total height (including the descent) needs to be returned. return page.width, page.height + page.descent, page.descent