fix cross-device link error
[PyX.git] / pyx / config.py
bloba3611743b803485d113e3ac55ab5db92a5f609bf
1 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2003-2011 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2011 André Wobst <wobsta@users.sourceforge.net>
7 # This file is part of PyX (http://pyx.sourceforge.net/).
9 # PyX is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
14 # PyX is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with PyX; if not, write to the Free Software
21 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
23 import configparser, io, logging, os, pkgutil, subprocess, shutil
25 logger = logging.getLogger("pyx")
26 logger_execute = logging.getLogger("pyx.execute")
27 logger_filelocator = logging.getLogger("pyx.filelocator")
29 builtinopen = open
31 try:
32 import pykpathsea as pykpathsea_module
33 has_pykpathsea = True
34 except ImportError:
35 has_pykpathsea = False
38 # Locators implement an open method which returns a list of functions
39 # by searching for a file according to a specific rule. Each of the functions
40 # returned can be called (multiple times) and return an open file. The
41 # opening of the file might fail with a IOError which indicates, that the
42 # file could not be found at the given location.
43 # names is a list of kpsewhich format names to be used for searching where as
44 # extensions is a list of file extensions to be tried (including the dot). Note
45 # that the list of file extenions should include an empty string to not add
46 # an extension at all.
48 locator_classes = {}
50 class local:
51 """locates files in the current directory"""
53 def openers(self, filename, names, extensions):
54 return [lambda extension=extension: builtinopen(filename+extension, "rb") for extension in extensions]
56 locator_classes["local"] = local
59 class internal:
60 """locates files within the PyX data tree"""
62 def openers(self, filename, names, extensions):
63 for extension in extensions:
64 full_filename = filename+extension
65 dir = os.path.splitext(full_filename)[1][1:]
66 try:
67 data = pkgutil.get_data("pyx", "data/%s/%s" % (dir, full_filename))
68 except IOError:
69 pass
70 else:
71 if data:
72 return [lambda: io.BytesIO(data)]
73 return []
75 locator_classes["internal"] = internal
78 class recursivedir:
79 """locates files by searching recursively in a list of directories"""
81 def __init__(self):
82 self.dirs = getlist("filelocator", "recursivedir")
83 self.full_filenames = {}
85 def openers(self, filename, names, extensions):
86 for extension in extensions:
87 if filename+extension in self.full_filenames:
88 return [lambda: builtinopen(self.full_filenames[filename+extension], "rb")]
89 while self.dirs:
90 dir = self.dirs.pop(0)
91 for item in os.listdir(dir):
92 full_item = os.path.join(dir, item)
93 if os.path.isdir(full_item):
94 self.dirs.insert(0, full_item)
95 else:
96 self.full_filenames[item] = full_item
97 for extension in extensions:
98 if filename+extension in self.full_filenames:
99 return [lambda: builtinopen(self.full_filenames[filename+extension], "rb")]
100 return []
102 locator_classes["recursivedir"] = recursivedir
105 class ls_R:
106 """locates files by searching a list of ls-R files"""
108 def __init__(self):
109 self.ls_Rs = getlist("filelocator", "ls-R")
110 self.full_filenames = {}
112 def openers(self, filename, names, extensions):
113 while self.ls_Rs and not any([filename+extension in self.full_filenames for extension in extensions]):
114 lsr = self.ls_Rs.pop(0)
115 base_dir = os.path.dirname(lsr)
116 dir = None
117 first = True
118 with builtinopen(lsr, "r", encoding="ascii", errors="surrogateescape") as lsrfile:
119 for line in lsrfile:
120 line = line.rstrip()
121 if first and line.startswith("%"):
122 continue
123 first = False
124 if line.endswith(":"):
125 dir = os.path.join(base_dir, line[:-1])
126 elif line:
127 self.full_filenames[line] = os.path.join(dir, line)
128 for extension in extensions:
129 if filename+extension in self.full_filenames:
130 def _opener():
131 try:
132 return builtinopen(self.full_filenames[filename+extension], "rb")
133 except IOError:
134 logger.warning("'%s' should be available at '%s' according to the ls-R file, "
135 "but the file is not available at this location; "
136 "update your ls-R file" % (filename, self.full_filenames[filename+extension]))
137 return [_opener]
138 return []
140 locator_classes["ls-R"] = ls_R
143 class pykpathsea:
144 """locate files by pykpathsea (a C extension module wrapping libkpathsea)"""
146 def openers(self, filename, names, extensions):
147 if not has_pykpathsea:
148 return []
149 for name in names:
150 full_filename = pykpathsea_module.find_file(filename, name)
151 if full_filename:
152 break
153 else:
154 return []
155 def _opener():
156 try:
157 return builtinopen(full_filename, "rb")
158 except IOError:
159 logger.warning("'%s' should be available at '%s' according to libkpathsea, "
160 "but the file is not available at this location; "
161 "update your kpsewhich database" % (filename, full_filename))
162 return [_opener]
164 locator_classes["pykpathsea"] = pykpathsea
167 # class libkpathsea:
168 # """locate files by libkpathsea using ctypes"""
170 # def openers(self, filename, names, extensions):
171 # raise NotImplemented
173 # locator_classes["libpathsea"] = libkpathsea
175 def Popen(cmd, *args, **kwargs):
176 try:
177 cmd + ""
178 except:
179 pass
180 else:
181 raise ValueError("pyx.config.Popen must not be used with a string cmd")
182 info = "PyX executes {} with args {}".format(cmd[0], cmd[1:])
183 try:
184 shutil.which
185 except:
186 pass
187 else:
188 info += " located at {}".format(shutil.which(cmd[0]))
189 logger_execute.info(info)
190 return subprocess.Popen(cmd, *args, **kwargs)
192 PIPE = subprocess.PIPE
193 STDOUT = subprocess.STDOUT
196 def fix_cygwin(full_filename):
197 # detect cygwin result on windows python
198 if os.name == "nt" and full_filename.startswith("/"):
199 with Popen(['cygpath', '-w', full_filename], stdout=subprocess.PIPE).stdout as output:
200 return io.TextIOWrapper(output, encoding="ascii", errors="surrogateescape").readline().rstrip()
201 return full_filename
204 class kpsewhich:
205 """locate files using the kpsewhich executable"""
207 def __init__(self):
208 self.kpsewhich = get("filelocator", "kpsewhich", "kpsewhich")
210 def openers(self, filename, names, extensions):
211 full_filename = None
212 for name in names:
213 try:
214 with Popen([self.kpsewhich, '--format', name, filename], stdout=subprocess.PIPE).stdout as output:
215 with io.TextIOWrapper(output, encoding="ascii", errors="surrogateescape") as text_output:
216 full_filename = text_output.readline().rstrip()
217 except OSError:
218 return []
219 if full_filename:
220 break
221 else:
222 return []
224 full_filename = fix_cygwin(full_filename)
226 def _opener():
227 try:
228 return builtinopen(full_filename, "rb")
229 except IOError:
230 logger.warning("'%s' should be available at '%s' according to kpsewhich, "
231 "but the file is not available at this location; "
232 "update your kpsewhich database" % (filename, full_filename))
233 return [_opener]
235 locator_classes["kpsewhich"] = kpsewhich
238 class locate:
239 """locate files using a locate executable"""
241 def __init__(self):
242 self.locate = get("filelocator", "locate", "locate")
244 def openers(self, filename, names, extensions):
245 full_filename = None
246 for extension in extensions:
247 with Popen([self.locate, filename+extension], stdout=subprocess.PIPE).stdout as output:
248 with io.TextIOWrapper(output, encoding="ascii", errors="surrogateescape") as text_output:
249 for line in text_output:
250 line = line.rstrip()
251 if os.path.basename(line) == filename+extension:
252 full_filename = line
253 break
254 if full_filename:
255 break
256 else:
257 return []
259 full_filename = fix_cygwin(full_filename)
261 def _opener():
262 try:
263 return builtinopen(full_filename, "rb")
264 except IOError:
265 logger.warning("'%s' should be available at '%s' according to the locate, "
266 "but the file is not available at this location; "
267 "update your locate database" % (filename+extension, full_filename))
268 return [_opener]
270 locator_classes["locate"] = locate
274 class _marker: pass
276 config = configparser.ConfigParser()
277 config.read_string(locator_classes["internal"]().openers("pyxrc", [], [""])[0]().read().decode("utf-8"), source="(internal pyxrc)")
278 if os.name == "nt":
279 user_pyxrc = os.path.join(os.environ['APPDATA'], "pyxrc")
280 else:
281 user_pyxrc = os.path.expanduser("~/.pyxrc")
282 config.read(user_pyxrc, encoding="utf-8")
283 if os.environ.get('PYXRC'):
284 config.read(os.environ['PYXRC'], encoding="utf-8")
286 def get(section, option, default=_marker):
287 if default is _marker:
288 return config.get(section, option)
289 else:
290 try:
291 return config.get(section, option)
292 except configparser.Error:
293 return default
295 def getint(section, option, default=_marker):
296 if default is _marker:
297 return config.getint(section, option)
298 else:
299 try:
300 return config.getint(section, option)
301 except configparser.Error:
302 return default
304 def getfloat(section, option, default=_marker):
305 if default is _marker:
306 return config.getfloat(section, option)
307 else:
308 try:
309 return config.getfloat(section, option)
310 except configparser.Error:
311 return default
313 def getboolean(section, option, default=_marker):
314 if default is _marker:
315 return config.getboolean(section, option)
316 else:
317 try:
318 return config.getboolean(section, option)
319 except configparser.Error:
320 return default
322 def getlist(section, option, default=_marker):
323 if default is _marker:
324 l = config.get(section, option).split()
325 else:
326 try:
327 l = config.get(section, option).split()
328 except configparser.Error:
329 return default
330 if space:
331 l = [item.replace(space, " ") for item in l]
332 return l
335 space = get("general", "space", "SPACE")
336 methods = [locator_classes[method]()
337 for method in getlist("filelocator", "methods", ["local", "internal", "pykpathsea", "kpsewhich"])]
338 opener_cache = {}
341 def open(filename, formats, ascii=False):
342 """returns an open file searched according the list of formats"""
344 # When using an empty list of formats, the names list is empty
345 # and the extensions list contains an empty string only. For that
346 # case some locators (notably local and internal) return an open
347 # function for the requested file whereas other locators might not
348 # return anything (like pykpathsea and kpsewhich).
349 # This is useful for files not to be searched in the latex
350 # installation at all (like lfs files).
351 extensions = set([""])
352 for format in formats:
353 for extension in format.extensions:
354 extensions.add(extension)
355 names = tuple([format.name for format in formats])
356 if (filename, names) in opener_cache:
357 file = opener_cache[(filename, names)]()
358 else:
359 for method in methods:
360 openers = method.openers(filename, names, extensions)
361 for opener in openers:
362 try:
363 file = opener()
364 except EnvironmentError:
365 file = None
366 if file:
367 info = "PyX filelocator found {} by method {}".format(filename, method.__class__.__name__)
368 if hasattr(file, "name"):
369 info += " at {}".format(file.name)
370 logger_filelocator.info(info)
371 opener_cache[(filename, names)] = opener
372 break
373 # break two loops here
374 else:
375 continue
376 break
377 else:
378 logger_filelocator.info("PyX filelocator failed to find {} of type {} and extensions {}".format(filename, names, extensions))
379 raise IOError("Could not locate the file '%s'." % filename)
380 if ascii:
381 return io.TextIOWrapper(file, encoding="ascii", errors="surrogateescape")
382 else:
383 return file
386 class format:
387 def __init__(self, name, extensions):
388 self.name = name
389 self.extensions = extensions
391 format.tfm = format("tfm", [".tfm"])
392 format.afm = format("afm", [".afm"])
393 format.fontmap = format("map", [])
394 format.pict = format("graphic/figure", [".eps", ".epsi"])
395 format.tex_ps_header = format("PostScript header", [".pro"]) # contains also: enc files
396 format.type1 = format("type1 fonts", [".pfa", ".pfb"])
397 format.vf = format("vf", [".vf"])
398 format.dvips_config = format("dvips config", [])