diff --git a/doc/api/next_api_changes/2018-02-16-JKS.rst b/doc/api/next_api_changes/2018-02-16-JKS.rst
new file mode 100644
index 000000000000..f38ad6d50932
--- /dev/null
+++ b/doc/api/next_api_changes/2018-02-16-JKS.rst
@@ -0,0 +1,8 @@
+dviread changes
+---------------
+
+The ``format`` keyword argument to ``dviread.find_tex_file`` has been
+deprecated. The function without the ``format`` argument, as well as
+the new ``dviread.find_tex_files`` function, cache their results in
+``texsupport.N.db`` in the cache directory to speed up dvi file
+processing.
diff --git a/doc/users/next_whats_new/texsupport_cache.rst b/doc/users/next_whats_new/texsupport_cache.rst
new file mode 100644
index 000000000000..b823e962a1d9
--- /dev/null
+++ b/doc/users/next_whats_new/texsupport_cache.rst
@@ -0,0 +1,22 @@
+TeX support cache
+-----------------
+
+The `usetex` feature sends snippets of TeX code to LaTeX and related
+external tools for processing. This causes a nontrivial number of
+helper processes to be spawned, which can be slow on some platforms.
+A new cache database helps reduce the need to spawn these helper
+processes, which should improve `usetex` processing speed.
+
+The new cache files
+~~~~~~~~~~~~~~~~~~~
+
+The cache database is stored in a file named `texsupport.N.db` in the
+standard cache directory (traditionally `$HOME/.matplotlib` but
+possibly `$HOME/.cache/matplotlib`), where `N` stands for a version
+number. The version number is incremented when new kinds of items are
+added to the caching code, in order to avoid version clashes when
+using multiple different versions of Matplotlib. The auxiliary files
+`texsupport.N.db-wal` and `texsupport.N.db-shm` help coordinate usage
+of the cache between concurrently running instances. All of these
+cache files may be deleted when Matplotlib is not running, and
+subsequent calls to the `usetex` code will recompute the TeX results.
diff --git a/doc/users/whats_new.rst b/doc/users/whats_new.rst
index 648daabc7c5b..3c5fdc23af73 100644
--- a/doc/users/whats_new.rst
+++ b/doc/users/whats_new.rst
@@ -14,12 +14,12 @@ revision, see the :ref:`github-stats`.
..
For a release, add a new section after this, then comment out the include
and toctree below by indenting them. Uncomment them after the release.
- .. include:: next_whats_new/README.rst
- .. toctree::
- :glob:
- :maxdepth: 1
+.. include:: next_whats_new/README.rst
+.. toctree::
+ :glob:
+ :maxdepth: 1
- next_whats_new/*
+ next_whats_new/*
New in Matplotlib 2.2
diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py
index e0048d8b8c3f..7d464209d06c 100644
--- a/lib/matplotlib/dviread.py
+++ b/lib/matplotlib/dviread.py
@@ -24,12 +24,13 @@
import os
import re
import struct
+import sqlite3
import sys
import textwrap
import numpy as np
-from matplotlib import cbook, rcParams
+from matplotlib import cbook, get_cachedir, rcParams
from matplotlib.compat import subprocess
_log = logging.getLogger(__name__)
@@ -170,8 +171,7 @@ def wrapper(self, byte):
class Dvi(object):
"""
A reader for a dvi ("device-independent") file, as produced by TeX.
- The current implementation can only iterate through pages in order,
- and does not even attempt to verify the postamble.
+ The current implementation can only iterate through pages in order.
This class can be used as a context manager to close the underlying
file upon exit. Pages can be read via iteration. Here is an overly
@@ -179,13 +179,26 @@ class Dvi(object):
>>> with matplotlib.dviread.Dvi('input.dvi', 72) as dvi:
>>> for page in dvi:
- >>> print(''.join(unichr(t.glyph) for t in page.text))
+ >>> print(''.join(chr(t.glyph) for t in page.text))
+
+ Parameters
+ ----------
+
+ filename : str
+ dvi file to read
+ dpi : number or None
+ Dots per inch, can be floating-point; this affects the
+ coordinates returned. Use None to get TeX's internal units
+ which are likely only useful for debugging.
+ cache : TeXSupportCache instance, optional
+ Support file cache instance, defaults to the TeXSupportCache
+ singleton.
"""
# dispatch table
_dtable = [None] * 256
_dispatch = partial(_dispatch, _dtable)
- def __init__(self, filename, dpi):
+ def __init__(self, filename, dpi, cache=None):
"""
Read the data from the file named *filename* and convert
TeX's internal units to units of *dpi* per inch.
@@ -193,11 +206,20 @@ def __init__(self, filename, dpi):
Use None to return TeX's internal units.
"""
_log.debug('Dvi: %s', filename)
+ if cache is None:
+ cache = TeXSupportCache.get_cache()
+ self.cache = cache
self.file = open(filename, 'rb')
self.dpi = dpi
self.fonts = {}
self.state = _dvistate.pre
self.baseline = self._get_baseline(filename)
+ self.fontnames = sorted(set(self._read_fonts()))
+ # populate kpsewhich cache with font pathnames
+ find_tex_files([x + suffix for x in self.fontnames
+ for suffix in ('.tfm', '.vf', '.pfb')],
+ cache)
+ cache.optimize()
def _get_baseline(self, filename):
if rcParams['text.latex.preview']:
@@ -205,8 +227,8 @@ def _get_baseline(self, filename):
baseline_filename = base + ".baseline"
if os.path.exists(baseline_filename):
with open(baseline_filename, 'rb') as fd:
- l = fd.read().split()
- height, depth, width = l
+ line = fd.read().split()
+ height, depth, width = line
return float(depth)
return None
@@ -293,6 +315,61 @@ def _output(self):
return Page(text=text, boxes=boxes, width=(maxx-minx)*d,
height=(maxy_pure-miny)*d, descent=descent)
+ def _read_fonts(self):
+ """Read the postamble of the file and return a list of fonts used."""
+
+ file = self.file
+ offset = -1
+ while offset > -100:
+ file.seek(offset, 2)
+ byte = file.read(1)[0]
+ if byte != 223:
+ break
+ offset -= 1
+ if offset >= -4:
+ raise ValueError(
+ "malformed dvi file %s: too few 223 bytes" % file.name)
+ if byte != 2:
+ raise ValueError(
+ ("malformed dvi file %s: post-postamble "
+ "identification byte not 2") % file.name)
+ file.seek(offset - 4, 2)
+ offset = struct.unpack('!I', file.read(4))[0]
+ file.seek(offset, 0)
+ try:
+ byte = file.read(1)[0]
+ except IndexError:
+ raise ValueError(
+ "malformed dvi file %s: postamble offset %d out of range"
+ % (file.name, offset))
+ if byte != 248:
+ raise ValueError(
+ "malformed dvi file %s: postamble not found at offset %d"
+ % (file.name, offset))
+
+ fonts = []
+ file.seek(28, 1)
+ while True:
+ byte = file.read(1)[0]
+ if 243 <= byte <= 246:
+ _, _, _, _, a, length = (
+ _arg_olen1(self, byte-243),
+ _arg(4, False, self, None),
+ _arg(4, False, self, None),
+ _arg(4, False, self, None),
+ _arg(1, False, self, None),
+ _arg(1, False, self, None))
+ fontname = file.read(a + length)[-length:].decode('ascii')
+ fonts.append(fontname)
+ elif byte == 249:
+ break
+ else:
+ raise ValueError(
+ "malformed dvi file %s: opcode %d in postamble"
+ % (file.name, byte))
+ file.seek(0, 0)
+ return fonts
+
def _read(self):
"""
Read one page from the file. Return True if successful,
@@ -592,6 +669,10 @@ class Vf(Dvi):
----------
filename : string or bytestring
+ vf file to read
+ cache : TeXSupportCache instance, optional
+ Support file cache instance, defaults to the TeXSupportCache
+ singleton.
Notes
-----
@@ -602,8 +683,8 @@ class Vf(Dvi):
but replaces the `_read` loop and dispatch mechanism.
"""
- def __init__(self, filename):
- Dvi.__init__(self, filename, 0)
+ def __init__(self, filename, cache=None):
+ Dvi.__init__(self, filename, dpi=0, cache=cache)
try:
self._first_font = None
self._chars = {}
@@ -614,6 +695,27 @@ def __init__(self, filename):
def __getitem__(self, code):
return self._chars[code]
+ def _read_fonts(self):
+ """Read through the font-definition section of the vf file
+ and return the list of font names."""
+ fonts = []
+ self.file.seek(0, 0)
+ while True:
+ byte = self.file.read(1)[0]
+ if byte <= 242 or byte >= 248:
+ break
+ elif 243 <= byte <= 246:
+ _ = self._arg(byte - 242)
+ _, _, _, a, length = [self._arg(x) for x in (4, 4, 4, 1, 1)]
+ fontname = self.file.read(a + length)[-length:].decode('ascii')
+ fonts.append(fontname)
+ elif byte == 247:
+ _, k = self._arg(1), self._arg(1)
+ _ = self.file.read(k)
+ _, _ = self._arg(4), self._arg(4)
+ self.file.seek(0, 0)
+ return fonts
+
def _read(self):
"""
Read one page from the file. Return True if successful,
@@ -651,8 +753,8 @@ def _read(self):
self._init_packet(packet_len)
elif 243 <= byte <= 246:
k = self._arg(byte - 242, byte == 246)
- c, s, d, a, l = [self._arg(x) for x in (4, 4, 4, 1, 1)]
- self._fnt_def_real(k, c, s, d, a, l)
+ c, s, d, a, length = [self._arg(x) for x in (4, 4, 4, 1, 1)]
+ self._fnt_def_real(k, c, s, d, a, length)
if self._first_font is None:
self._first_font = k
elif byte == 247: # preamble
@@ -980,45 +1082,268 @@ def _parse(self, file):
return re.findall(br'/([^][{}<>\s]+)', data)
-def find_tex_file(filename, format=None):
+class TeXSupportCacheError(Exception):
+ pass
+
+
+class TeXSupportCache:
+ """A persistent cache of data related to support files related to dvi
+ files produced by TeX. Currently holds results from :program:`kpsewhich`,
+ in future versions could hold pre-parsed font data etc.
+
+ Usage::
+
+ # create or get the singleton instance
+ cache = TeXSupportCache.get_cache()
+ with cache.connection as transaction:
+ cache.update_pathnames(
+ {"pdftex.map": "/usr/local/pdftex.map",
+ "cmsy10.pfb": "/usr/local/fonts/cmsy10.pfb"},
+ transaction)
+ pathnames = cache.get_pathnames(["pdftex.map", "cmr10.pfb"])
+ # now pathnames = {"pdftex.map": "/usr/local/pdftex.map"}
+
+ # optional after inserting new data, may improve query performance:
+ cache.optimize()
+
+ Parameters
+ ----------
+
+ filename : str, optional
+ File in which to store the cache. Defaults to `texsupport.N.db` in
+ the standard cache directory where N is the current schema version.
+
+ Attributes
+ ----------
+
+ connection
+ This database connection object has a context manager to set up
+ a transaction. Transactions are passed into methods that write to
+ the database.
"""
- Find a file in the texmf tree.
+
+ __slots__ = ('connection')
+ schema_version = 1 # should match PRAGMA user_version in _create
+ instance = None
+
+ @classmethod
+ def get_cache(cls):
+ "Return the singleton instance of the cache, at the default location"
+ if cls.instance is None:
+ cls.instance = cls()
+ return cls.instance
+
+ def __init__(self, filename=None):
+ if filename is None:
+ filename = os.path.join(get_cachedir(), 'texsupport.%d.db'
+ % self.schema_version)
+
+ self.connection = sqlite3.connect(
+ filename, isolation_level="DEFERRED")
+ if _log.isEnabledFor(logging.DEBUG):
+ def debug_sql(sql):
+ _log.debug(' '.join(sql.splitlines()).strip())
+ self.connection.set_trace_callback(debug_sql)
+ self.connection.row_factory = sqlite3.Row
+ with self.connection as conn:
+ conn.executescript("""
+ PRAGMA journal_mode=WAL;
+ PRAGMA synchronous=NORMAL;
+ PRAGMA foreign_keys=ON;
+ """)
+ version, = conn.execute("PRAGMA user_version;").fetchone()
+
+ if version == 0:
+ self._create()
+ elif version != self.schema_version:
+ raise TeXSupportCacheError(
+ "support database %s has version %d, expected %d"
+ % (filename, version, self.schema_version))
+
+ def _create(self):
+ """Create the database."""
+ with self.connection as conn:
+ conn.executescript(
+ """
+ PRAGMA page_size=4096;
+ CREATE TABLE file_path(
+ filename TEXT PRIMARY KEY NOT NULL,
+ pathname TEXT
+ ) WITHOUT ROWID;
+ PRAGMA user_version=1;
+ """)
+
+ def optimize(self):
+ """Optional optimization phase after updating data.
+ Executes sqlite's `PRAGMA optimize` statement, which can call
+ `ANALYZE` or other functions that can improve future query performance
+ by spending some time up-front."""
+ with self.connection as conn:
+ conn.execute("PRAGMA optimize;")
+
+ def get_pathnames(self, filenames):
+ """Query the cache for pathnames related to `filenames`.
+
+ Parameters
+ ----------
+ filenames : iterable of str
+
+ Returns
+ -------
+ mapping from str to (str or None)
+ For those filenames that exist in the cache, the mapping
+ includes either the related pathname or None to indicate that
+ the named file does not exist.
+ """
+ rows = self.connection.execute(
+ "SELECT filename, pathname FROM file_path WHERE filename IN "
+ "(%s)"
+ % ','.join('?' for _ in filenames),
+ filenames).fetchall()
+ return {filename: pathname for (filename, pathname) in rows}
+
+ def update_pathnames(self, mapping, transaction):
+ """Update the cache with the given filename-to-pathname mapping
+
+ Parameters
+ ----------
+ mapping : mapping from str to (str or None)
+ Mapping from filenames to the corresponding full pathnames
+ or None to indicate that the named file does not exist.
+ transaction : obtained via the context manager of self.connection
+ """
+ transaction.executemany(
+ "INSERT OR REPLACE INTO file_path (filename, pathname) "
+ "VALUES (?, ?)",
+ mapping.items())
+
+
+def find_tex_files(filenames, cache=None):
+ """Find multiple files in the texmf tree. This can be more efficient
+ than `find_tex_file` because it makes only one call to `kpsewhich`.
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 results are cached into the TeX support database. In case of
+ mistaken results, deleting the database resets the cache.
+
Parameters
----------
filename : string or bytestring
- format : string or bytestring
- Used as the value of the `--format` option to :program:`kpsewhich`.
- Could be e.g. 'tfm' or 'vf' to limit the search to that type of files.
+ cache : TeXSupportCache, optional
+ Cache instance to use, defaults to the singleton instance of the class.
References
----------
.. [1] `Kpathsea documentation `_
The library that :program:`kpsewhich` is part of.
+
"""
# we expect these to always be ascii encoded, but use utf-8
# out of caution
- if isinstance(filename, bytes):
- filename = filename.decode('utf-8', errors='replace')
- if isinstance(format, bytes):
- format = format.decode('utf-8', errors='replace')
+ filenames = [f.decode('utf-8', errors='replace')
+ if isinstance(f, bytes) else f
+ for f in filenames]
+ if cache is None:
+ cache = TeXSupportCache.get_cache()
+ result = cache.get_pathnames(filenames)
+
+ filenames = [f for f in filenames if f not in result]
+ if not filenames:
+ return result
- cmd = ['kpsewhich']
- if format is not None:
- cmd += ['--format=' + format]
- cmd += [filename]
- _log.debug('find_tex_file(%s): %s', filename, cmd)
+ cmd = ['kpsewhich'] + list(filenames)
+ _log.debug('find_tex_files: %s', cmd)
pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
- result = pipe.communicate()[0].rstrip()
- _log.debug('find_tex_file result: %s', result)
- return result.decode('ascii')
+ output = pipe.communicate()[0].decode('ascii').splitlines()
+ _log.debug('find_tex_files result: %s', output)
+ mapping = _match(filenames, output)
+ with cache.connection as transaction:
+ cache.update_pathnames(mapping, transaction)
+ result.update(mapping)
+
+ return result
+
+
+def _match(filenames, pathnames):
+ """
+ Match filenames to pathnames in lists that are in matching order,
+ except that some filenames may lack pathnames.
+ """
+ result = {f: None for f in filenames}
+ filenames, pathnames = iter(filenames), iter(pathnames)
+ try:
+ filename, pathname = next(filenames), next(pathnames)
+ while True:
+ if pathname.endswith(os.path.sep + filename):
+ result[filename] = pathname
+ pathname = next(pathnames)
+ filename = next(filenames)
+ except StopIteration:
+ return result
+
+
+def find_tex_file(filename, format=None, cache=None):
+ """
+ Find a file in the texmf tree.
+
+ 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 results are cached into a database whose location defaults to
+ :file:`~/.matplotlib/texsupport.db`. In case of mistaken results,
+ deleting this file resets the cache.
+
+ Parameters
+ ----------
+ filename : string or bytestring
+ format : string or bytestring, DEPRECATED
+ Used as the value of the `--format` option to :program:`kpsewhich`.
+ Could be e.g. 'tfm' or 'vf' to limit the search to that type of files.
+ Deprecated to allow batching multiple filenames into one kpsewhich
+ call, since any format option would apply to all filenames at once.
+ cache : TeXSupportCache, optional
+ Cache instance to use, defaults to the singleton instance of the class.
+
+ References
+ ----------
+
+ .. [1] `Kpathsea documentation `_
+ The library that :program:`kpsewhich` is part of.
+ """
+
+ if format is not None:
+ cbook.warn_deprecated(
+ "3.0",
+ "The format option to find_tex_file is deprecated "
+ "to allow batching multiple filenames into one call. "
+ "Omitting the option should not change the result, as "
+ "kpsewhich uses the filename extension to choose the path.")
+ # we expect these to always be ascii encoded, but use utf-8
+ # out of caution
+ if isinstance(filename, bytes):
+ filename = filename.decode('utf-8', errors='replace')
+ if isinstance(format, bytes):
+ format = format.decode('utf-8', errors='replace')
+
+ cmd = ['kpsewhich']
+ if format is not None:
+ cmd += ['--format=' + format]
+ cmd += [filename]
+ _log.debug('find_tex_file(%s): %s', filename, cmd)
+ pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+ result = pipe.communicate()[0].rstrip()
+ _log.debug('find_tex_file result: %s', result)
+ return result.decode('ascii')
+
+ return list(find_tex_files([filename], cache).values())[0]
# With multiple text objects per figure (e.g., tick labels) we may end
diff --git a/lib/matplotlib/tests/baseline_images/dviread/broken1.dvi b/lib/matplotlib/tests/baseline_images/dviread/broken1.dvi
new file mode 100644
index 000000000000..6e960f435de9
Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/dviread/broken1.dvi differ
diff --git a/lib/matplotlib/tests/baseline_images/dviread/broken2.dvi b/lib/matplotlib/tests/baseline_images/dviread/broken2.dvi
new file mode 100644
index 000000000000..bd2b74795346
Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/dviread/broken2.dvi differ
diff --git a/lib/matplotlib/tests/baseline_images/dviread/broken3.dvi b/lib/matplotlib/tests/baseline_images/dviread/broken3.dvi
new file mode 100644
index 000000000000..5c64bcc7d332
Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/dviread/broken3.dvi differ
diff --git a/lib/matplotlib/tests/baseline_images/dviread/broken4.dvi b/lib/matplotlib/tests/baseline_images/dviread/broken4.dvi
new file mode 100644
index 000000000000..79c30e331244
Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/dviread/broken4.dvi differ
diff --git a/lib/matplotlib/tests/baseline_images/dviread/broken5.dvi b/lib/matplotlib/tests/baseline_images/dviread/broken5.dvi
new file mode 100644
index 000000000000..7d7fdcbd8f02
Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/dviread/broken5.dvi differ
diff --git a/lib/matplotlib/tests/baseline_images/dviread/virtual.vf b/lib/matplotlib/tests/baseline_images/dviread/virtual.vf
new file mode 100644
index 000000000000..2b64f1df3da6
Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/dviread/virtual.vf differ
diff --git a/lib/matplotlib/tests/baseline_images/dviread/virtual.vpl b/lib/matplotlib/tests/baseline_images/dviread/virtual.vpl
new file mode 100644
index 000000000000..0c051a508b65
--- /dev/null
+++ b/lib/matplotlib/tests/baseline_images/dviread/virtual.vpl
@@ -0,0 +1,23 @@
+(FAMILY TESTING)
+(COMMENT Test data for matplotlib)
+(COMMENT Run vptovf virtual.vpl to obtain virtual.vf)
+(FACE O 352)
+(CODINGSCHEME TEX TEXT)
+(DESIGNSIZE R 10.0)
+(FONTDIMEN
+ (SLANT R 0.0)
+ (SPACE R 0.333334)
+ (STRETCH R 0.166667)
+ (SHRINK R 0.111112)
+ (XHEIGHT R 0.430555)
+ (QUAD R 1.000003)
+ (EXTRASPACE R 0.111112)
+ )
+(MAPFONT D 0
+ (FONTNAME cmr10)
+ (FONTDSIZE R 10.0)
+ )
+(MAPFONT D 1
+ (FONTNAME cmex10)
+ (FONTDSIZE R 10.0)
+ )
diff --git a/lib/matplotlib/tests/test_dviread.py b/lib/matplotlib/tests/test_dviread.py
index 6b005fd34170..6091c106db22 100644
--- a/lib/matplotlib/tests/test_dviread.py
+++ b/lib/matplotlib/tests/test_dviread.py
@@ -1,9 +1,16 @@
from matplotlib.testing.decorators import skip_if_command_unavailable
+try:
+ from unittest import mock
+except ImportError:
+ import mock
+
import matplotlib.dviread as dr
import os.path
import json
import pytest
+import sqlite3
+import warnings
def test_PsfontsMap(monkeypatch):
@@ -68,3 +75,102 @@ def test_dviread():
'boxes': [[b.x, b.y, b.height, b.width] for b in page.boxes]}
for page in dvi]
assert data == correct
+
+
+@skip_if_command_unavailable(["kpsewhich", "-version"])
+def test_dviread_get_fonts():
+ dir = os.path.join(os.path.dirname(__file__), 'baseline_images', 'dviread')
+ with dr.Dvi(os.path.join(dir, 'test.dvi'), None) as dvi:
+ assert dvi.fontnames == \
+ ['cmex10', 'cmmi10', 'cmmi5', 'cmr10', 'cmr5', 'cmr7']
+ with dr.Vf(os.path.join(dir, 'virtual.vf')) as vf:
+ assert vf.fontnames == ['cmex10', 'cmr10']
+
+
+def test_dviread_get_fonts_error_handling():
+ dir = os.path.join(os.path.dirname(__file__), 'baseline_images', 'dviread')
+ for n, message in [(1, "too few 223 bytes"),
+ (2, "post-postamble identification"),
+ (3, "postamble offset"),
+ (4, "postamble not found"),
+ (5, "opcode 127 in postamble")]:
+ with pytest.raises(ValueError) as e:
+ dr.Dvi(os.path.join(dir, "broken%d.dvi" % n), None)
+ assert message in str(e.value)
+
+
+def test_TeXSupportCache(tmpdir):
+ dbfile = str(tmpdir / "test.db")
+ cache = dr.TeXSupportCache(filename=dbfile)
+ assert cache.get_pathnames(['foo', 'bar']) == {}
+ with cache.connection as transaction:
+ cache.update_pathnames({'foo': '/tmp/foo',
+ 'xyzzy': '/xyzzy.dat',
+ 'fontfile': None}, transaction)
+ assert cache.get_pathnames(['foo', 'bar']) == {'foo': '/tmp/foo'}
+ assert cache.get_pathnames(['xyzzy', 'fontfile']) == \
+ {'xyzzy': '/xyzzy.dat', 'fontfile': None}
+
+
+def test_TeXSupportCache_versioning(tmpdir):
+ dbfile = str(tmpdir / "test.db")
+ cache1 = dr.TeXSupportCache(dbfile)
+ with cache1.connection as transaction:
+ cache1.update_pathnames({'foo': '/tmp/foo'}, transaction)
+
+ with sqlite3.connect(dbfile, isolation_level="DEFERRED") as conn:
+ conn.executescript('PRAGMA user_version=1000000000;')
+
+ with pytest.raises(dr.TeXSupportCacheError):
+ cache2 = dr.TeXSupportCache(dbfile)
+
+
+def test_find_tex_files(tmpdir):
+ with mock.patch('matplotlib.dviread.subprocess.Popen') as mock_popen:
+ mock_proc = mock.Mock()
+ stdout = '{s}tmp{s}foo.pfb\n{s}tmp{s}bar.map\n'.\
+ format(s=os.path.sep).encode('ascii')
+ mock_proc.configure_mock(**{'communicate.return_value': (stdout, b'')})
+ mock_popen.return_value = mock_proc
+
+ # first call uses the results from kpsewhich
+ cache = dr.TeXSupportCache(filename=str(tmpdir / "test.db"))
+ assert dr.find_tex_files(
+ ['foo.pfb', 'cmsy10.pfb', 'bar.tmp', 'bar.map'], cache) \
+ == {'foo.pfb': '{s}tmp{s}foo.pfb'.format(s=os.path.sep),
+ 'bar.map': '{s}tmp{s}bar.map'.format(s=os.path.sep),
+ 'cmsy10.pfb': None, 'bar.tmp': None}
+ assert mock_popen.called
+
+ # second call (subset of the first one) uses only the cache
+ mock_popen.reset_mock()
+ assert dr.find_tex_files(['foo.pfb', 'cmsy10.pfb'], cache) \
+ == {'foo.pfb': '{s}tmp{s}foo.pfb'.format(s=os.path.sep),
+ 'cmsy10.pfb': None}
+ assert not mock_popen.called
+
+ # third call (includes more than the first one) uses kpsewhich again
+ mock_popen.reset_mock()
+ stdout = '{s}usr{s}local{s}cmr10.tfm\n'.\
+ format(s=os.path.sep).encode('ascii')
+ mock_proc.configure_mock(**{'communicate.return_value': (stdout, b'')})
+ mock_popen.return_value = mock_proc
+ assert dr.find_tex_files(['foo.pfb', 'cmr10.tfm'], cache) == \
+ {'foo.pfb': '{s}tmp{s}foo.pfb'.format(s=os.path.sep),
+ 'cmr10.tfm': '{s}usr{s}local{s}cmr10.tfm'.format(s=os.path.sep)}
+ assert mock_popen.called
+
+
+def test_find_tex_file_format():
+ with mock.patch('matplotlib.dviread.subprocess.Popen') as mock_popen:
+ mock_proc = mock.Mock()
+ stdout = b'/foo/bar/baz\n'
+ mock_proc.configure_mock(**{'communicate.return_value': (stdout, b'')})
+ mock_popen.return_value = mock_proc
+
+ warnings.filterwarnings(
+ 'ignore',
+ 'The format option to find_tex_file is deprecated.*',
+ UserWarning)
+ assert dr.find_tex_file('foobar', format='tfm') == '/foo/bar/baz'
+ assert mock_popen.called