Skip to content

Reuse single kpsewhich instance for speed. #19531

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ jobs:

# Install dependencies from PyPI.
python -mpip install --upgrade $PRE \
cycler kiwisolver numpy pillow pyparsing python-dateutil \
cycler kiwisolver numpy pillow ptyprocess pyparsing python-dateutil \
-r requirements/testing/all.txt \
${{ matrix.extra-requirements }}

Expand Down
3 changes: 3 additions & 0 deletions doc/api/next_api_changes/deprecations/19531-AL.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The *format* kwarg to ``dviread.find_tex_file``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
... is deprecated.
90 changes: 72 additions & 18 deletions lib/matplotlib/dviread.py
Original file line number Diff line number Diff line change
Expand Up @@ -1040,7 +1040,50 @@ def _parse_enc(path):
"Failed to parse {} as Postscript encoding".format(path))


class _Kpsewhich:
@lru_cache() # A singleton.
def __new__(cls):
self = object.__new__(cls)
self._proc = self._new_proc()
return self

def _new_proc(self):
from ptyprocess import PtyProcess # Force unbuffered IO.
try:
# kpsewhich requires being passed at least argument, so query
# texmf.cnf and read that out. This also serves as sentinel below.
proc = PtyProcess.spawn(
["kpsewhich", "-interactive", "texmf.cnf"], echo=False)
self._sentinel = proc.readline()
return proc
except FileNotFoundError:
return None

def search(self, filename):
if filename == "texmf.cnf":
return self._sentinel
if self._proc is not None and not self._proc.isalive():
self._proc = self._new_proc() # Give a 2nd chance if it crashed.
if self._proc is None or not self._proc.isalive():
return ""
# kpsewhich prints nothing if the file does not exist. To detect this
# case without setting an arbitrary timeout, we additionally query for
# texmf.cnf, which is known to exist at `self._sentinel`. We can then
# check whether the first query exists by comparing the first returned
# line to `self._sentinel`. (This is also why we need to separately
# handle the case of querying texmf.cnf above.)
self._proc.write(os.fsencode(filename) + b"\ntexmf.cnf\n")
out = self._proc.readline()
if out == self._sentinel:
return ""
else:
self._proc.readline() # flush the extra sentinel line.
# POSIX ptys actually emit \r\n.
return os.fsdecode(out).rstrip("\r\n")


@lru_cache()
@_api.delete_parameter("3.5", "format")
def find_tex_file(filename, format=None):
"""
Find a file in the texmf tree.
Expand Down Expand Up @@ -1072,25 +1115,36 @@ def find_tex_file(filename, format=None):
if isinstance(format, bytes):
format = format.decode('utf-8', errors='replace')

if os.name == 'nt':
if format is not None: # Deprecated.
if os.name == 'nt': # See below re: encoding.
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]
try:
result = cbook._check_and_log_subprocess(cmd, _log, **kwargs)
except (FileNotFoundError, RuntimeError):
return ''
return result.rstrip('\n')

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]
try:
result = cbook._check_and_log_subprocess(cmd, _log, **kwargs)
except (FileNotFoundError, RuntimeError):
return ''
return result.rstrip('\n')
# The `command_line_encoding` environment variable is set to force
# it to always use utf-8 encoding. See Matplotlib issue #11848.
try:
result = cbook._check_and_log_subprocess(
["kpsewhich", filename], _log, encoding="utf-8",
env={**os.environ, "command_line_encoding": "utf-8"})
except (FileNotFoundError, RuntimeError):
return ""
return result.rstrip("\n")
else:
return _Kpsewhich().search(filename)


@lru_cache()
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ def build_extensions(self):
"pillow>=6.2.0",
"pyparsing>=2.2.1",
"python-dateutil>=2.7",
"ptyprocess;os_name=='posix'",
],

cmdclass=cmdclass,
Expand Down