1 # Copyright (C) 2002-2013 Jörg Lehmann <joergl@users.sourceforge.net>
2 # Copyright (C) 2003-2011 Michael Schindler <m-schindler@users.sourceforge.net>
3 # Copyright (C) 2002-2013 André Wobst <wobsta@users.sourceforge.net>
5 # This file is part of PyX (http://pyx.sourceforge.net/).
7 # PyX is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
12 # PyX is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with PyX; if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
21 import atexit
, errno
, functools
, glob
, inspect
, io
, itertools
, logging
, os
22 import queue
, re
, shutil
, sys
, tempfile
, textwrap
, threading
24 from pyx
import config
, unit
, box
, baseclasses
, trafo
, version
, attr
, style
, path
25 from pyx
import bbox
as bboxmodule
26 from pyx
.dvi
import dvifile
28 logger
= logging
.getLogger("pyx")
30 def remove_string(p
, s
):
31 """Removes a string from a string.
33 The function removes the first occurrence of a string in another string.
35 :param str p: string to be removed
36 :param str s: string to be searched
37 :returns: tuple of the result string and a success boolean (``True`` when
38 the string was removed)
39 :rtype: tuple of str and bool
42 >>> remove_string("XXX", "abcXXXdefXXXghi")
43 ('abcdefXXXghi', True)
46 r
= s
.replace(p
, '', 1)
50 def remove_pattern(p
, s
, ignore_nl
=True):
51 r
"""Removes a pattern from a string.
53 The function removes the first occurence of the pattern from a string. It
54 returns a tuple of the resulting string and the matching object (or
55 ``None``, if the pattern did not match).
57 :param re.regex p: pattern to be removed
58 :param str s: string to be searched
59 :param bool ignore_nl: When ``True``, newlines in the string are ignored
60 during the pattern search. The returned string will still contain all
61 newline characters outside of the matching part of the string, whereas
62 the returned matching object will not contain the newline characters
63 inside of the matching part of the string.
64 :returns: the result string and the match object or ``None`` if
66 :rtype: tuple of str and (re.match or None)
69 >>> r, m = remove_pattern(re.compile("XXX"), 'ab\ncXX\nXdefXX\nX')
72 >>> m.string[m.start():m.end()]
77 r
= s
.replace('\n', '')
84 s_start
= r_start
= m
.start()
85 s_end
= r_end
= m
.end()
96 return s
[:s_start
] + s
[s_end
:], m
101 """Return list of positions of a character in a string.
104 >>> index_all("X", "abXcdXef")
109 return [i
for i
, x
in enumerate(s
) if x
== c
]
113 """Returns iterator over pairs of data from an iterable.
116 >>> list(pairwise([1, 2, 3]))
120 a
, b
= itertools
.tee(i
)
125 def remove_nested_brackets(s
, openbracket
="(", closebracket
=")", quote
='"'):
126 """Remove nested brackets
128 Return a modified string with all nested brackets 1 removed, i.e. only
129 keep the first bracket nesting level. In case an opening bracket is
130 immediately followed by a quote, the quoted string is left untouched,
131 even if it contains brackets. The use-case for that are files in the
132 folder "Program Files (x86)".
134 If the bracket nesting level is broken (unbalanced), the unmodified
138 >>> remove_nested_brackets('aaa("bb()bb" cc(dd(ee))ff)ggg'*2)
139 'aaa("bb()bb" ccff)gggaaa("bb()bb" ccff)ggg'
142 openpos
= index_all(openbracket
, s
)
143 closepos
= index_all(closebracket
, s
)
144 if quote
is not None:
145 quotepos
= index_all(quote
, s
)
146 for openquote
, closequote
in pairwise(quotepos
):
147 if openquote
-1 in openpos
:
148 # ignore brackets in quoted string
149 openpos
= [pos
for pos
in openpos
150 if not (openquote
< pos
< closequote
)]
151 closepos
= [pos
for pos
in closepos
152 if not (openquote
< pos
< closequote
)]
153 if len(openpos
) != len(closepos
):
154 # unbalanced brackets
157 # keep the original string in case we need to return due to broken nesting levels
161 # Iterate over the bracket positions from the end.
162 # We go reversely to be able to immediately remove nested bracket levels
163 # without influencing bracket positions yet to come in the loop.
164 for pos
, leveldelta
in sorted(itertools
.chain(zip(openpos
, itertools
.repeat(-1)),
165 zip(closepos
, itertools
.repeat(1))),
167 # the current bracket nesting level
170 # unbalanced brackets
172 if leveldelta
== 1 and level
== 2:
173 # a closing bracket to cut after
175 if leveldelta
== -1 and level
== 1:
176 # an opening bracket to cut at -> remove
177 r
= r
[:pos
] + r
[endpos
:]
181 class TexResultError(ValueError):
182 "Error raised by :class:`texmessage` parsers."
187 """Collection of TeX output parsers.
189 This class is not meant to be instanciated. Instead, it serves as a
190 namespace for TeX output parsers, which are functions receiving a TeX
191 output and returning parsed output.
193 In addition, this class also contains some generator functions (namely
194 :attr:`texmessage.no_file` and :attr:`texmessage.pattern`), which return a
195 function according to the given parameters. They are used to generate some
196 of the parsers in this class and can be used to create others as well.
199 start_pattern
= re
.compile(r
"This is [-0-9a-zA-Z\s_]*TeX")
203 r
"""Validate TeX/LaTeX startup message including scrollmode test.
206 >>> texmessage.start(r'''
207 ... This is e-TeX (version)
208 ... *! Undefined control sequence.
215 # check for "This is e-TeX" etc.
216 if not texmessage
.start_pattern
.search(msg
):
217 raise TexResultError("TeX startup failed")
219 # check for \raiseerror -- just to be sure that communication works
220 new
= msg
.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[-1]
222 raise TexResultError("TeX scrollmode check failed")
226 def no_file(fileending
, qualname
=None):
227 "Generator function to ignore the missing file message for fileending."
229 "Ignore the missing {} file message."
230 return msg
.replace("No file texput.%s." % fileending
, "").replace("No file %s%stexput.%s." % (os
.curdir
, os
.sep
, fileending
), "")
231 check
.__doc
__ = check
.__doc
__.format(fileending
)
232 if qualname
is not None:
233 check
.__qualname
__ = qualname
236 no_aux
= staticmethod(no_file
.__func
__("aux", "texmessage.no_aux"))
237 no_nav
= staticmethod(no_file
.__func
__("nav", "texmessage.no_nav"))
239 aux_pattern
= re
.compile(r
'\(([^()]+\.aux|"[^"]+\.aux")\)')
240 log_pattern
= re
.compile(r
"Transcript written on .*texput\.log\.", re
.DOTALL
)
244 "Validate TeX shutdown message."
245 msg
= re
.sub(texmessage
.aux_pattern
, "", msg
).replace("(see the transcript file for additional information)", "")
247 # check for "Transcript written on ...log."
248 msg
, m
= remove_pattern(texmessage
.log_pattern
, msg
)
250 raise TexResultError("TeX logfile message expected")
253 quoted_file_pattern
= re
.compile(r
'\("(?P<filename>[^"]+)".*?\)')
254 file_pattern
= re
.compile(r
'\((?P<filename>[^"][^ )]*).*?\)', re
.DOTALL
)
258 """Ignore file loading messages.
260 Removes text starting with a round bracket followed by a filename
261 ignoring all further text until the corresponding closing bracket.
262 Quotes and/or line breaks in the filename are handled as needed for TeX
265 Without quoting the filename, the necessary removal of line breaks is
266 not well defined and the different possibilities are tested to check
267 whether one solution is ok. The last of the examples below checks this
271 >>> texmessage.load(r'''other (text.py) things''', 0)
273 >>> texmessage.load(r'''other ("text.py") things''', 0)
275 >>> texmessage.load(r'''other ("tex
276 ... t.py" further (ignored)
277 ... text) things''', 0)
279 >>> texmessage.load(r'''other (t
283 ... ther (ignored) text) things''', 0)
287 r
= remove_nested_brackets(msg
)
288 r
, m
= remove_pattern(texmessage
.quoted_file_pattern
, r
)
290 if not os
.path
.isfile(m
.group("filename")):
292 r
, m
= remove_pattern(texmessage
.quoted_file_pattern
, r
)
293 r
, m
= remove_pattern(texmessage
.file_pattern
, r
, ignore_nl
=False)
295 for filename
in itertools
.accumulate(m
.group("filename").split("\n")):
296 if os
.path
.isfile(filename
):
300 r
, m
= remove_pattern(texmessage
.file_pattern
, r
, ignore_nl
=False)
303 quoted_def_pattern
= re
.compile(r
'\("(?P<filename>[^"]+\.(fd|def))"\)')
304 def_pattern
= re
.compile(r
'\((?P<filename>[^"][^ )]*\.(fd|def))\)')
308 "Ignore font definition (``*.fd`` and ``*.def``) loading messages."
310 for p
in [texmessage
.quoted_def_pattern
, texmessage
.def_pattern
]:
311 r
, m
= remove_pattern(p
, r
)
313 if not os
.path
.isfile(m
.group("filename")):
315 r
, m
= remove_pattern(texmessage
.quoted_file_pattern
, r
)
318 quoted_graphics_pattern
= re
.compile(r
'<"(?P<filename>[^"]+\.eps)">')
319 graphics_pattern
= re
.compile(r
'<(?P<filename>[^"][^>]*\.eps)>')
322 def load_graphics(msg
):
323 "Ignore graphics file (``*.eps``) loading messages."
325 for p
in [texmessage
.quoted_graphics_pattern
, texmessage
.graphics_pattern
]:
326 r
, m
= remove_pattern(p
, r
)
328 if not os
.path
.isfile(m
.group("filename")):
330 r
, m
= remove_pattern(texmessage
.quoted_file_pattern
, r
)
335 """Ignore all messages.
337 Should be used as a last resort only. You should write a proper TeX
338 output parser function for the output you observe.
345 """Warn about all messages.
347 Similar to :attr:`ignore`, but writing a warning to the logger about
348 the TeX output. This is considered to be better when you need to get it
349 working quickly as you will still be prompted about the unresolved
350 output, while the processing continues.
354 logger
.warning("ignoring TeX warnings:\n%s" % textwrap
.indent(msg
.rstrip(), " "))
358 def pattern(p
, warning
, qualname
=None):
359 "Warn by regular expression pattern matching."
362 msg
, m
= remove_pattern(p
, msg
, ignore_nl
=False)
364 logger
.warning("ignoring %s:\n%s" % (warning
, m
.string
[m
.start(): m
.end()].rstrip()))
365 msg
, m
= remove_pattern(p
, msg
, ignore_nl
=False)
367 check
.__doc
__ = check
.__doc
__.format(warning
)
368 if qualname
is not None:
369 check
.__qualname
__ = qualname
372 box_warning
= staticmethod(pattern
.__func
__(re
.compile(r
"^(Overfull|Underfull) \\[hv]box.*$(\n^..*$)*\n^$\n", re
.MULTILINE
),
373 "overfull/underfull box", qualname
="texmessage.box_warning"))
374 font_warning
= staticmethod(pattern
.__func
__(re
.compile(r
"^LaTeX Font Warning: .*$(\n^\(Font\).*$)*", re
.MULTILINE
),
375 "font substitutions of NFSS", qualname
="texmessage.font_warning"))
376 package_warning
= staticmethod(pattern
.__func
__(re
.compile(r
"^package\s+(?P<packagename>\S+)\s+warning\s*:[^\n]+(?:\n\(?(?P=packagename)\)?[^\n]*)*", re
.MULTILINE | re
.IGNORECASE
),
377 "generic package messages", qualname
="texmessage.package_warning"))
378 rerun_warning
= staticmethod(pattern
.__func
__(re
.compile(r
"^(LaTeX Warning: Label\(s\) may have changed\. Rerun to get cross-references right\s*\.)$", re
.MULTILINE
),
379 "rerun required message", qualname
="texmessage.rerun_warning"))
380 nobbl_warning
= staticmethod(pattern
.__func
__(re
.compile(r
"^[\s\*]*(No file .*\.bbl.)\s*", re
.MULTILINE
),
381 "no-bbl message", qualname
="texmessage.nobbl_warning"))
384 ###############################################################################
386 ###############################################################################
388 _textattrspreamble
= ""
391 "a textattr defines a apply method, which modifies a (La)TeX expression"
393 class _localattr
: pass
395 _textattrspreamble
+= r
"""\gdef\PyXFlushHAlign{0}%
397 \leftskip=0pt plus \PyXFlushHAlign fil%
398 \rightskip=0pt plus 1fil%
399 \advance\rightskip0pt plus -\PyXFlushHAlign fil%
405 \exhyphenpenalty=9999}%
408 class boxhalign(attr
.exclusiveattr
, textattr
, _localattr
):
410 def __init__(self
, aboxhalign
):
411 self
.boxhalign
= aboxhalign
412 attr
.exclusiveattr
.__init
__(self
, boxhalign
)
414 def apply(self
, expr
):
415 return r
"\gdef\PyXBoxHAlign{%.5f}%s" % (self
.boxhalign
, expr
)
417 boxhalign
.left
= boxhalign(0)
418 boxhalign
.center
= boxhalign(0.5)
419 boxhalign
.right
= boxhalign(1)
420 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass for boxhalign since it can't clear a halign's boxhalign
423 class flushhalign(attr
.exclusiveattr
, textattr
, _localattr
):
425 def __init__(self
, aflushhalign
):
426 self
.flushhalign
= aflushhalign
427 attr
.exclusiveattr
.__init
__(self
, flushhalign
)
429 def apply(self
, expr
):
430 return r
"\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self
.flushhalign
, expr
)
432 flushhalign
.left
= flushhalign(0)
433 flushhalign
.center
= flushhalign(0.5)
434 flushhalign
.right
= flushhalign(1)
435 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
438 class halign(boxhalign
, flushhalign
, _localattr
):
440 def __init__(self
, aboxhalign
, aflushhalign
):
441 self
.boxhalign
= aboxhalign
442 self
.flushhalign
= aflushhalign
443 attr
.exclusiveattr
.__init
__(self
, halign
)
445 def apply(self
, expr
):
446 return r
"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self
.boxhalign
, self
.flushhalign
, expr
)
448 halign
.left
= halign(0, 0)
449 halign
.center
= halign(0.5, 0.5)
450 halign
.right
= halign(1, 1)
451 halign
.clear
= attr
.clearclass(halign
)
452 halign
.boxleft
= boxhalign
.left
453 halign
.boxcenter
= boxhalign
.center
454 halign
.boxright
= boxhalign
.right
455 halign
.flushleft
= halign
.raggedright
= flushhalign
.left
456 halign
.flushcenter
= halign
.raggedcenter
= flushhalign
.center
457 halign
.flushright
= halign
.raggedleft
= flushhalign
.right
460 class _mathmode(attr
.attr
, textattr
, _localattr
):
463 def apply(self
, expr
):
464 return r
"$\displaystyle{%s}$" % expr
466 mathmode
= _mathmode()
467 clearmathmode
= attr
.clearclass(_mathmode
)
470 class _phantom(attr
.attr
, textattr
, _localattr
):
473 def apply(self
, expr
):
474 return r
"\phantom{%s}" % expr
477 clearphantom
= attr
.clearclass(_phantom
)
480 _textattrspreamble
+= "\\newbox\\PyXBoxVBox%\n\\newdimen\\PyXDimenVBox%\n"
482 class parbox_pt(attr
.sortbeforeexclusiveattr
, textattr
):
488 def __init__(self
, width
, baseline
=top
):
489 self
.width
= width
* 72.27 / (unit
.scale
["x"] * 72)
490 self
.baseline
= baseline
491 attr
.sortbeforeexclusiveattr
.__init
__(self
, parbox_pt
, [_localattr
])
493 def apply(self
, expr
):
494 if self
.baseline
== self
.top
:
495 return r
"\linewidth=%.5ftruept\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self
.width
, expr
)
496 elif self
.baseline
== self
.middle
:
497 return r
"\linewidth=%.5ftruept\setbox\PyXBoxVBox=\hbox{{\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}}}\PyXDimenVBox=0.5\dp\PyXBoxVBox\setbox\PyXBoxVBox=\hbox{{\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}}}\advance\PyXDimenVBox by -0.5\dp\PyXBoxVBox\lower\PyXDimenVBox\box\PyXBoxVBox" % (self
.width
, expr
, expr
)
498 elif self
.baseline
== self
.bottom
:
499 return r
"\linewidth=%.5ftruept\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self
.width
, expr
)
501 ValueError("invalid baseline argument")
503 parbox_pt
.clear
= attr
.clearclass(parbox_pt
)
505 class parbox(parbox_pt
):
507 def __init__(self
, width
, **kwargs
):
508 parbox_pt
.__init
__(self
, unit
.topt(width
), **kwargs
)
510 parbox
.clear
= parbox_pt
.clear
513 _textattrspreamble
+= "\\newbox\\PyXBoxVAlign%\n\\newdimen\\PyXDimenVAlign%\n"
515 class valign(attr
.sortbeforeexclusiveattr
, textattr
):
517 def __init__(self
, avalign
):
518 self
.valign
= avalign
519 attr
.sortbeforeexclusiveattr
.__init
__(self
, valign
, [parbox_pt
, _localattr
])
521 def apply(self
, expr
):
522 return r
"\setbox\PyXBoxVAlign=\hbox{{%s}}\PyXDimenVAlign=%.5f\ht\PyXBoxVAlign\advance\PyXDimenVAlign by -%.5f\dp\PyXBoxVAlign\lower\PyXDimenVAlign\box\PyXBoxVAlign" % (expr
, 1-self
.valign
, self
.valign
)
524 valign
.top
= valign(0)
525 valign
.middle
= valign(0.5)
526 valign
.bottom
= valign(1)
527 valign
.clear
= valign
.baseline
= attr
.clearclass(valign
)
530 _textattrspreamble
+= "\\newdimen\\PyXDimenVShift%\n"
532 class _vshift(attr
.sortbeforeattr
, textattr
):
535 attr
.sortbeforeattr
.__init
__(self
, [valign
, parbox_pt
, _localattr
])
537 def apply(self
, expr
):
538 return r
"%s\setbox0\hbox{{%s}}\lower\PyXDimenVShift\box0" % (self
.setheightexpr(), expr
)
540 class vshift(_vshift
):
541 "vertical down shift by a fraction of a character height"
543 def __init__(self
, lowerratio
, heightstr
="0"):
544 _vshift
.__init
__(self
)
545 self
.lowerratio
= lowerratio
546 self
.heightstr
= heightstr
548 def setheightexpr(self
):
549 return r
"\setbox0\hbox{{%s}}\PyXDimenVShift=%.5f\ht0" % (self
.heightstr
, self
.lowerratio
)
551 class _vshiftmathaxis(_vshift
):
552 "vertical down shift by the height of the math axis"
554 def setheightexpr(self
):
555 return r
"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\PyXDimenVShift=\ht0"
558 vshift
.bottomzero
= vshift(0)
559 vshift
.middlezero
= vshift(0.5)
560 vshift
.topzero
= vshift(1)
561 vshift
.mathaxis
= _vshiftmathaxis()
562 vshift
.clear
= attr
.clearclass(_vshift
)
565 defaultsizelist
= ["normalsize", "large", "Large", "LARGE", "huge", "Huge",
566 None, "tiny", "scriptsize", "footnotesize", "small"]
568 class size(attr
.sortbeforeattr
, textattr
):
571 def __init__(self
, sizeindex
=None, sizename
=None, sizelist
=defaultsizelist
):
572 if (sizeindex
is None and sizename
is None) or (sizeindex
is not None and sizename
is not None):
573 raise ValueError("either specify sizeindex or sizename")
574 attr
.sortbeforeattr
.__init
__(self
, [_mathmode
, _vshift
])
575 if sizeindex
is not None:
576 if sizeindex
>= 0 and sizeindex
< sizelist
.index(None):
577 self
.size
= sizelist
[sizeindex
]
578 elif sizeindex
< 0 and sizeindex
+ len(sizelist
) > sizelist
.index(None):
579 self
.size
= sizelist
[sizeindex
]
581 raise IndexError("index out of sizelist range")
585 def apply(self
, expr
):
586 return r
"\%s{}%s" % (self
.size
, expr
)
589 size
.scriptsize
= size
.script
= size(-3)
590 size
.footnotesize
= size
.footnote
= size(-2)
591 size
.small
= size(-1)
592 size
.normalsize
= size
.normal
= size(0)
598 size
.clear
= attr
.clearclass(size
)
601 ###############################################################################
603 ###############################################################################
606 class MonitorOutput(threading
.Thread
):
608 def __init__(self
, name
, output
):
609 """Deadlock-safe output stream reader and monitor.
611 This method sets up a thread to continously read lines from a stream.
612 By that a deadlock due to a full pipe is prevented. In addition, the
613 stream content can be monitored for containing a certain string (see
614 :meth:`expect` and :meth:`wait`) and return all the collected output
617 :param string name: name to be used while logging in :meth:`wait` and
619 :param file output: output stream
623 self
._expect
= queue
.Queue(1)
624 self
._received
= threading
.Event()
625 self
._output
= queue
.Queue()
626 threading
.Thread
.__init
__(self
, name
=name
, daemon
=1)
630 """Expect a string on a **single** line in the output.
632 This method must be called **before** the output occurs, i.e. before
633 the input is written to the TeX/LaTeX process.
635 :param s: expected string or ``None`` if output is expected to become
640 self
._expect
.put_nowait(s
)
643 """Read all output collected since its previous call.
645 The output reading should be synchronized by the :meth:`expect`
646 and :meth:`wait` methods.
648 :returns: collected output from the stream
655 l
.append(self
._output
.get_nowait())
658 return "".join(l
).replace("\r\n", "\n").replace("\r", "\n")
660 def _wait(self
, waiter
, checker
):
661 """Helper method to implement :meth:`wait` and :meth:`done`.
663 Waits for an event using the *waiter* and *checker* functions while
664 providing user feedback to the ``pyx``-logger using the warning level
665 according to the ``wait`` and ``showwait`` from the ``text`` section of
666 the pyx :mod:`config`.
668 :param function waiter: callback to wait for (the function gets called
669 with a timeout parameter)
670 :param function checker: callback returing ``True`` if
671 waiting was successful
672 :returns: ``True`` when wait was successful
676 wait
= config
.getint("text", "wait", 60)
677 showwait
= config
.getint("text", "showwait", 5)
681 while waited
< wait
and not hasevent
:
682 if wait
- waited
> showwait
:
686 waiter(wait
- waited
)
687 waited
+= wait
- waited
691 logger
.warning("Still waiting for {} "
692 "after {} (of {}) seconds..."
693 .format(self
.name
, waited
, wait
))
695 logger
.warning("The timeout of {} seconds expired "
696 "and {} did not respond."
697 .format(waited
, self
.name
))
704 """Wait for the expected output to happen.
706 Waits either until a line containing the string set by the previous
707 :meth:`expect` call is found, or a timeout occurs.
709 :returns: ``True`` when the expected string was found
713 r
= self
._wait
(self
._received
.wait
, self
._received
.isSet
)
715 self
._received
.clear()
719 """Waits until the output becomes empty.
721 Waits either until the output becomes empty, or a timeout occurs.
722 The generated output can still be catched by :meth:`read` after
723 :meth:`done` was successful.
725 In the proper workflow :meth:`expect` should be called with ``None``
726 before the output completes, as otherwise a ``ValueError`` is raised
729 :returns: ``True`` when the output has become empty
733 return self
._wait
(self
.join
, lambda self
=self
: not self
.is_alive())
736 """Read a line from the output.
738 To be used **inside** the thread routine only.
740 :returns: one line of the output as a string
746 return self
.output
.readline()
748 if e
.errno
!= errno
.EINTR
:
754 **Not** to be called from outside.
756 :raises ValueError: output becomes empty while some string is expected
761 line
= self
._readline
()
764 expect
= self
._expect
.get_nowait()
769 self
._output
.put(line
)
770 if expect
is not None:
771 found
= line
.find(expect
)
776 if expect
is not None:
777 raise ValueError("{} finished unexpectedly".format(self
.name
))
780 class textbox(box
.rect
, baseclasses
.canvasitem
):
781 """basically a box.rect, but it contains a text created by the texrunner
782 - texrunner._text and texrunner.text return such an object
783 - _textbox instances can be inserted into a canvas
784 - the output is contained in a page of the dvifile available thru the texrunner"""
785 # TODO: shouldn't all boxes become canvases? how about inserts then?
787 def __init__(self
, x
, y
, left
, right
, height
, depth
, do_finish
, fontmap
, singlecharmode
, attrs
):
789 - do_finish is a method to be called to get the dvicanvas
790 (e.g. the do_finish calls the setdvicanvas method)
791 - attrs are fillstyles"""
794 self
.width
= left
+ right
797 self
.do_finish
= do_finish
798 self
.fontmap
= fontmap
799 self
.singlecharmode
= singlecharmode
802 self
.texttrafo
= trafo
.scale(unit
.scale
["x"]).translated(x
, y
)
803 box
.rect
.__init
__(self
, x
- left
, y
- depth
, left
+ right
, depth
+ height
, abscenter
= (left
, depth
))
805 self
.dvicanvas
= None
806 self
.insertdvicanvas
= False
808 def transform(self
, *trafos
):
809 if self
.dvicanvas
is not None:
810 raise ValueError("can't apply after dvicanvas was inserted")
811 box
.rect
.transform(self
, *trafos
)
813 self
.texttrafo
= trafo
* self
.texttrafo
815 def readdvipage(self
, dvifile
, page
):
816 self
.dvicanvas
= dvifile
.readpage([ord("P"), ord("y"), ord("X"), page
, 0, 0, 0, 0, 0, 0],
817 fontmap
=self
.fontmap
, singlecharmode
=self
.singlecharmode
, attrs
=[self
.texttrafo
] + self
.attrs
)
819 def marker(self
, marker
):
821 return self
.texttrafo
.apply(*self
.dvicanvas
.markers
[marker
])
825 textpath
= path
.path()
826 for item
in self
.dvicanvas
.items
:
827 textpath
+= item
.textpath()
828 return textpath
.transformed(self
.texttrafo
)
830 def processPS(self
, file, writer
, context
, registry
, bbox
):
832 abbox
= bboxmodule
.empty()
833 self
.dvicanvas
.processPS(file, writer
, context
, registry
, abbox
)
834 bbox
+= box
.rect
.bbox(self
)
836 def processPDF(self
, file, writer
, context
, registry
, bbox
):
838 abbox
= bboxmodule
.empty()
839 self
.dvicanvas
.processPDF(file, writer
, context
, registry
, abbox
)
840 bbox
+= box
.rect
.bbox(self
)
848 "Constants defining the verbosity of the :exc:`TexResultError`."
849 none
= 0 #: Without any input and output.
850 default
= 1 #: Input and parsed output shortend to 5 lines.
851 full
= 2 #: Full input and unparsed as well as parsed output.
856 def __init__(self
, *files
):
859 def write(self
, data
):
860 for file in self
.files
:
864 for file in self
.files
:
868 for file in self
.files
:
871 # The texrunner state represents the next (or current) execute state.
872 STATE_START
, STATE_PREAMBLE
, STATE_TYPESET
, STATE_DONE
= range(4)
873 PyXBoxPattern
= re
.compile(r
"PyXBox:page=(?P<page>\d+),lt=(?P<lt>-?\d*((\d\.?)|(\.?\d))\d*)pt,rt=(?P<rt>-?\d*((\d\.?)|(\.?\d))\d*)pt,ht=(?P<ht>-?\d*((\d\.?)|(\.?\d))\d*)pt,dp=(?P<dp>-?\d*((\d\.?)|(\.?\d))\d*)pt:")
874 dvi_pattern
= re
.compile(r
"Output written on .*texput\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\.", re
.DOTALL
)
876 class TexDoneError(Exception):
882 texmessages_start_default
= [texmessage
.start
]
883 #: default :class:`texmessage` parsers for interpreter startup
884 texmessages_start_default
= [texmessage
.start
]
885 #: default :class:`texmessage` parsers for interpreter shutdown
886 texmessages_end_default
= [texmessage
.end
, texmessage
.font_warning
, texmessage
.rerun_warning
, texmessage
.nobbl_warning
]
887 #: default :class:`texmessage` parsers for preamble output
888 texmessages_preamble_default
= [texmessage
.load
]
889 #: default :class:`texmessage` parsers for typeset output
890 texmessages_run_default
= [texmessage
.font_warning
, texmessage
.box_warning
, texmessage
.package_warning
,
891 texmessage
.load_def
, texmessage
.load_graphics
]
893 def __init__(self
, executable
,
896 texipc
=config
.getboolean("text", "texipc", 0),
899 errordetail
=errordetail
.default
,
900 texmessages_start
=[],
902 texmessages_preamble
=[],
904 """Base class for the TeX interface.
906 .. note:: This class cannot be used directly. It is the base class for
907 all texrunners and provides most of the implementation.
908 Still, to the end user the parameters except for *executable*
909 are important, as they are preserved in derived classes
912 :param str executable: command to start the TeX interpreter
913 :param str texenc: encoding to use in the communication with the TeX
915 :param usefiles: list of supplementary files
916 :type usefiles: list of str
917 :param bool texipc: :ref:`texipc` flag.
918 :param copyinput: filename or file to be used to store a copy of all
919 the input passed to the TeX interpreter
920 :type copyinput: None or str or file
921 :param bool dvitype: flag to turn on dvitype-like output
922 :param errordetail: verbosity of the :exc:`TexResultError`
923 :type errordetail: :class:`errordetail`
924 :param texmessages_start: additional message parsers at interpreter
926 :type texmessages_start: list of :class:`texmessage` parsers
927 :param texmessages_end: additional message parsers at interpreter
929 :type texmessages_end: list of :class:`texmessage` parsers
930 :param texmessages_preamble: additional message parsers for preamble
932 :type texmessages_preamble: list of :class:`texmessage` parsers
933 :param texmessages_run: additional message parsers for typset output
934 :type texmessages_run: list of :class:`texmessage` parsers
936 self
.executable
= executable
938 self
.usefiles
= usefiles
940 self
.copyinput
= copyinput
941 self
.dvitype
= dvitype
942 self
.errordetail
= errordetail
943 self
.texmessages_start
= texmessages_start
944 self
.texmessages_end
= texmessages_end
945 self
.texmessages_preamble
= texmessages_preamble
946 self
.texmessages_run
= texmessages_run
948 self
.state
= STATE_START
952 self
.needdvitextboxes
= [] # when texipc-mode off
958 """Clean-up TeX interpreter and tmp directory.
960 This funtion is hooked up in atexit to quit the TeX interpreter, to
961 save contents of usefiles, and to remove the temporary directory.
965 if self
.state
> STATE_START
:
966 if self
.state
< STATE_DONE
:
968 if self
.state
< STATE_DONE
: # cleanup while TeX is still running?
969 self
.texoutput
.expect(None)
971 for f
, msg
in [(self
.texinput
.close
, "We tried to communicate to %s to quit itself, but this seem to have failed. Trying by terminate signal now ...".format(self
.name
)),
972 (self
.popen
.terminate
, "Failed, too. Trying by kill signal now ..."),
973 (self
.popen
.kill
, "We tried very hard to get rid of the %s process, but we ultimately failed (as far as we can tell). Sorry.".format(self
.name
))]:
975 if self
.texoutput
.done():
978 for usefile
in self
.usefiles
:
979 extpos
= usefile
.rfind(".")
981 os
.rename(os
.path
.join(self
.tmpdir
, "texput" + usefile
[extpos
:]), usefile
)
982 except EnvironmentError:
983 logger
.warning("Could not save '{}'.".format(usefile
))
984 if os
.path
.isfile(usefile
):
987 except EnvironmentError:
988 logger
.warning("Failed to remove spurious file '{}'.".format(usefile
))
990 shutil
.rmtree(self
.tmpdir
, ignore_errors
=True)
992 def _execute(self
, expr
, texmessages
, oldstate
, newstate
):
993 """executes expr within TeX/LaTeX"""
994 assert STATE_PREAMBLE
<= oldstate
<= STATE_TYPESET
995 assert oldstate
== self
.state
996 assert newstate
>= oldstate
997 if newstate
== STATE_DONE
:
998 self
.texoutput
.expect(None)
999 self
.texinput
.write(expr
)
1001 if oldstate
== newstate
== STATE_TYPESET
:
1003 expr
= "\\ProcessPyXBox{%s%%\n}{%i}" % (expr
, self
.page
)
1005 self
.texoutput
.expect("PyXInputMarker:executeid=%i:" % self
.executeid
)
1006 expr
+= "%%\n\\PyXInput{%i}%%\n" % self
.executeid
1007 self
.texinput
.write(expr
)
1008 self
.texinput
.flush()
1009 self
.state
= newstate
1010 if newstate
== STATE_DONE
:
1011 wait_ok
= self
.texoutput
.done()
1013 wait_ok
= self
.texoutput
.wait()
1015 parsed
= unparsed
= self
.texoutput
.read()
1017 raise TexResultError("TeX didn't respond as expected within the timeout period.")
1018 if newstate
!= STATE_DONE
:
1019 parsed
, m
= remove_string("PyXInputMarker:executeid=%s:" % self
.executeid
, parsed
)
1021 raise TexResultError("PyXInputMarker expected")
1022 if oldstate
== newstate
== STATE_TYPESET
:
1023 parsed
, m
= remove_pattern(PyXBoxPattern
, parsed
, ignore_nl
=False)
1025 raise TexResultError("PyXBox expected")
1026 if m
.group("page") != str(self
.page
):
1027 raise TexResultError("Wrong page number in PyXBox")
1028 extent
= [float(x
)*72/72.27*unit
.x_pt
for x
in m
.group("lt", "rt", "ht", "dp")]
1029 parsed
, m
= remove_string("[80.121.88.%s]" % self
.page
, parsed
)
1031 raise TexResultError("PyXPageOutMarker expected")
1033 # check for "Output written on ...dvi (1 page, 220 bytes)."
1035 parsed
, m
= remove_pattern(dvi_pattern
, parsed
)
1037 raise TexResultError("TeX dvifile messages expected")
1038 if m
.group("page") != str(self
.page
):
1039 raise TexResultError("wrong number of pages reported")
1041 parsed
, m
= remove_string("No pages of output.", parsed
)
1043 raise TexResultError("no dvifile expected")
1045 for t
in texmessages
:
1047 if parsed
.replace(r
"(Please type a command or say `\end')", "").replace(" ", "").replace("*\n", "").replace("\n", ""):
1048 raise TexResultError("unhandled TeX response (might be an error)")
1049 except TexResultError
as e
:
1050 if self
.errordetail
> errordetail
.none
:
1051 def add(msg
): e
.args
= (e
.args
[0] + msg
,)
1052 add("\nThe expression passed to TeX was:\n{}".format(textwrap
.indent(expr
.rstrip(), " ")))
1053 if self
.errordetail
== errordetail
.full
:
1054 add("\nThe return message from TeX was:\n{}".format(textwrap
.indent(unparsed
.rstrip(), " ")))
1055 if self
.errordetail
== errordetail
.default
:
1056 if parsed
.count('\n') > 6:
1057 parsed
= "\n".join(parsed
.split("\n")[:5] + ["(cut after 5 lines; use errordetail.full for all output)"])
1058 add("\nAfter parsing the return message from TeX, the following was left:\n{}".format(textwrap
.indent(parsed
.rstrip(), " ")))
1060 if oldstate
== newstate
== STATE_TYPESET
:
1064 assert self
.state
== STATE_START
1065 self
.state
= STATE_PREAMBLE
1067 if self
.tmpdir
is None:
1068 self
.tmpdir
= tempfile
.mkdtemp()
1069 atexit
.register(self
._cleanup
)
1070 for usefile
in self
.usefiles
:
1071 extpos
= usefile
.rfind(".")
1073 os
.rename(usefile
, os
.path
.join(self
.tmpdir
, "texput" + usefile
[extpos
:]))
1076 cmd
= [self
.executable
, '--output-directory', self
.tmpdir
]
1079 self
.popen
= config
.Popen(cmd
, stdin
=config
.PIPE
, stdout
=config
.PIPE
, stderr
=config
.STDOUT
, bufsize
=0)
1080 self
.texinput
= io
.TextIOWrapper(self
.popen
.stdin
, encoding
=self
.texenc
)
1083 self
.copyinput
.write
1084 except AttributeError:
1085 self
.texinput
= Tee(open(self
.copyinput
, "w", encoding
=self
.texenc
), self
.texinput
)
1087 self
.texinput
= Tee(self
.copyinput
, self
.texinput
)
1088 self
.texoutput
= MonitorOutput(self
.name
, io
.TextIOWrapper(self
.popen
.stdout
, encoding
=self
.texenc
))
1089 self
._execute
("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
1090 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
1091 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
1092 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
1093 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
1094 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
1095 "\\newdimen\\PyXDimenHAlignRT%\n" +
1096 _textattrspreamble
+ # insert preambles for textattrs macros
1097 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
1098 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
1099 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
1100 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
1101 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
1102 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
1103 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
1104 "lt=\\the\\PyXDimenHAlignLT,"
1105 "rt=\\the\\PyXDimenHAlignRT,"
1106 "ht=\\the\\ht\\PyXBox,"
1107 "dp=\\the\\dp\\PyXBox:}%\n"
1108 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
1109 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
1110 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
1111 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
1112 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%", # write PyXMarker special into the dvi-file
1113 self
.texmessages_start_default
+ self
.texmessages_start
, STATE_PREAMBLE
, STATE_PREAMBLE
)
1115 def do_preamble(self
, expr
, texmessages
):
1116 if self
.state
< STATE_PREAMBLE
:
1118 self
._execute
(expr
, texmessages
, STATE_PREAMBLE
, STATE_PREAMBLE
)
1120 def do_typeset(self
, expr
, texmessages
):
1121 if self
.state
< STATE_PREAMBLE
:
1123 if self
.state
< STATE_TYPESET
:
1125 return self
._execute
(expr
, texmessages
, STATE_TYPESET
, STATE_TYPESET
)
1127 def do_finish(self
):
1128 if self
.state
== STATE_DONE
:
1130 if self
.state
< STATE_TYPESET
:
1133 self
.texinput
.close() # close the input queue and
1134 self
.texoutput
.done() # wait for finish of the output
1137 dvifilename
= os
.path
.join(self
.tmpdir
, "texput.dvi")
1138 self
.dvifile
= dvifile
.DVIfile(dvifilename
, debug
=self
.dvitype
)
1140 for box
in self
.needdvitextboxes
:
1141 box
.readdvipage(self
.dvifile
, page
)
1143 if self
.dvifile
.readpage(None) is not None:
1144 raise ValueError("end of dvifile expected but further pages follow")
1146 def preamble(self
, expr
, texmessages
=[]):
1147 r
"""Execute a preamble.
1149 :param str expr: expression to be executed
1150 :param texmessages: additional message parsers
1151 :type texmessages: list of :class:`texmessage` parsers
1153 Preambles must not generate output, but are used to load files, perform
1154 settings, define macros, *etc*. In LaTeX mode, preambles are executed
1155 before ``\begin{document}``. The method can be called multiple times,
1156 but only prior to :meth:`SingleRunner.text` and
1157 :meth:`SingleRunner.text_pt`.
1160 texmessages
= self
.texmessages_preamble_default
+ self
.texmessages_preamble
+ texmessages
1161 self
.do_preamble(expr
, texmessages
)
1163 def text(self
, x
, y
, expr
, textattrs
=[], texmessages
=[], fontmap
=None, singlecharmode
=False):
1164 """create text by passing expr to TeX/LaTeX
1165 - returns a textbox containing the result from running expr thru TeX/LaTeX
1166 - the box center is set to x, y
1167 - *args may contain attr parameters, namely:
1168 - textattr instances
1169 - texmessage instances
1170 - trafo._trafo instances
1171 - style.fillstyle instances"""
1172 if self
.state
== STATE_DONE
:
1173 raise TexDoneError("typesetting process was terminated already")
1174 textattrs
= attr
.mergeattrs(textattrs
) # perform cleans
1175 attr
.checkattrs(textattrs
, [textattr
, trafo
.trafo_pt
, style
.fillstyle
])
1176 trafos
= attr
.getattrs(textattrs
, [trafo
.trafo_pt
])
1177 fillstyles
= attr
.getattrs(textattrs
, [style
.fillstyle
])
1178 textattrs
= attr
.getattrs(textattrs
, [textattr
])
1179 for ta
in textattrs
[::-1]:
1180 expr
= ta
.apply(expr
)
1181 first
= self
.state
< STATE_TYPESET
1182 left
, right
, height
, depth
= self
.do_typeset(expr
, self
.texmessages_run_default
+ self
.texmessages_run
+ texmessages
)
1183 if self
.texipc
and first
:
1184 self
.dvifile
= dvifile
.DVIfile(os
.path
.join(self
.tmpdir
, "texput.dvi"), debug
=self
.dvitype
)
1185 box
= textbox(x
, y
, left
, right
, height
, depth
, self
.do_finish
, fontmap
, singlecharmode
, fillstyles
)
1187 box
.reltransform(t
) # TODO: should trafos really use reltransform???
1188 # this is quite different from what we do elsewhere!!!
1189 # see https://sourceforge.net/mailarchive/forum.php?thread_id=9137692&forum_id=23700
1191 box
.readdvipage(self
.dvifile
, self
.page
)
1193 self
.needdvitextboxes
.append(box
)
1196 def text_pt(self
, x
, y
, expr
, *args
, **kwargs
):
1197 return self
.text(x
* unit
.t_pt
, y
* unit
.t_pt
, expr
, *args
, **kwargs
)
1200 class SingleTexRunner(SingleRunner
):
1202 def __init__(self
, executable
=config
.get("text", "tex", "tex"), lfs
="10pt", **kwargs
):
1203 super().__init
__(executable
=executable
, **kwargs
)
1207 def go_typeset(self
):
1208 assert self
.state
== STATE_PREAMBLE
1209 self
.state
= STATE_TYPESET
1211 def go_finish(self
):
1212 self
._execute
("\\end%\n", self
.texmessages_end_default
+ self
.texmessages_end
, STATE_TYPESET
, STATE_DONE
)
1214 def force_done(self
):
1215 self
.texinput
.write("\n\\end\n")
1220 if not self
.lfs
.endswith(".lfs"):
1221 self
.lfs
= "%s.lfs" % self
.lfs
1222 with config
.open(self
.lfs
, []) as lfsfile
:
1223 lfsdef
= lfsfile
.read().decode("ascii")
1224 self
._execute
(lfsdef
, [], STATE_PREAMBLE
, STATE_PREAMBLE
)
1225 self
._execute
("\\normalsize%\n", [], STATE_PREAMBLE
, STATE_PREAMBLE
)
1226 self
._execute
("\\newdimen\\linewidth\\newdimen\\textwidth%\n", [], STATE_PREAMBLE
, STATE_PREAMBLE
)
1229 class SingleLatexRunner(SingleRunner
):
1231 texmessages_docclass_default
= [texmessage
.load
]
1232 texmessages_begindoc_default
= [texmessage
.load
, texmessage
.no_aux
]
1234 def __init__(self
, executable
=config
.get("text", "latex", "latex"),
1235 docclass
="article", docopt
=None, pyxgraphics
=True,
1236 texmessages_docclass
=[], texmessages_begindoc
=[], **kwargs
):
1237 super().__init
__(executable
=executable
, **kwargs
)
1238 self
.docclass
= docclass
1239 self
.docopt
= docopt
1240 self
.pyxgraphics
= pyxgraphics
1241 self
.texmessages_docclass
= texmessages_docclass
1242 self
.texmessages_begindoc
= texmessages_begindoc
1245 def go_typeset(self
):
1246 self
._execute
("\\begin{document}", self
.texmessages_begindoc_default
+ self
.texmessages_begindoc
, STATE_PREAMBLE
, STATE_TYPESET
)
1248 def go_finish(self
):
1249 self
._execute
("\\end{document}%\n", self
.texmessages_end_default
+ self
.texmessages_end
, STATE_TYPESET
, STATE_DONE
)
1251 def force_done(self
):
1252 self
.texinput
.write("\n\\catcode`\\@11\\relax\\@@end\n")
1256 if self
.pyxgraphics
:
1257 with config
.open("pyx.def", []) as source
, open(os
.path
.join(self
.tmpdir
, "pyx.def"), "wb") as dest
:
1258 dest
.write(source
.read())
1259 self
._execute
("\\makeatletter%\n"
1260 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
1261 "\\def\\ProcessOptions{%\n"
1262 "\\def\\Gin@driver{" + self
.tmpdir
.replace(os
.sep
, "/") + "/pyx.def}%\n"
1263 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
1264 "\\saveProcessOptions}%\n"
1266 [], STATE_PREAMBLE
, STATE_PREAMBLE
)
1267 if self
.docopt
is not None:
1268 self
._execute
("\\documentclass[%s]{%s}" % (self
.docopt
, self
.docclass
),
1269 self
.texmessages_docclass_default
+ self
.texmessages_docclass
, STATE_PREAMBLE
, STATE_PREAMBLE
)
1271 self
._execute
("\\documentclass{%s}" % self
.docclass
,
1272 self
.texmessages_docclass_default
+ self
.texmessages_docclass
, STATE_PREAMBLE
, STATE_PREAMBLE
)
1275 def reset_for_tex_done(f
):
1277 def wrapped(self
, *args
, **kwargs
):
1279 return f(self
, *args
, **kwargs
)
1280 except TexDoneError
:
1281 self
.reset(reinit
=True)
1282 return f(self
, *args
, **kwargs
)
1288 def __init__(self
, cls
, *args
, **kwargs
):
1291 self
.kwargs
= kwargs
1294 def preamble(self
, expr
, texmessages
=[]):
1295 self
.preambles
.append((expr
, texmessages
))
1296 self
.instance
.preamble(expr
, texmessages
)
1299 def text_pt(self
, *args
, **kwargs
):
1300 return self
.instance
.text_pt(*args
, **kwargs
)
1303 def text(self
, *args
, **kwargs
):
1304 return self
.instance
.text(*args
, **kwargs
)
1306 def reset(self
, reinit
=False):
1307 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
1308 self
.instance
= self
.cls(*self
.args
, **self
.kwargs
)
1310 for expr
, texmessages
in self
.preambles
:
1311 self
.instance
.preamble(expr
, texmessages
)
1316 class TexRunner(MultiRunner
):
1318 def __init__(self
, **kwargs
):
1319 super().__init
__(SingleTexRunner
, **kwargs
)
1322 class LatexRunner(MultiRunner
):
1324 def __init__(self
, **kwargs
):
1325 super().__init
__(SingleLatexRunner
, **kwargs
)
1328 # old, deprecated names:
1329 texrunner
= TexRunner
1330 latexrunner
= LatexRunner
1332 def set(mode
="tex", **kwargs
):
1333 # note: defaulttexrunner is deprecated
1334 global default_runner
, defaulttexrunner
, reset
, preamble
, text
, text_pt
1337 default_runner
= defaulttexrunner
= TexRunner(**kwargs
)
1338 elif mode
== "latex":
1339 default_runner
= defaulttexrunner
= LatexRunner(**kwargs
)
1341 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1342 reset
= default_runner
.reset
1343 preamble
= default_runner
.preamble
1344 text
= default_runner
.text
1345 text_pt
= default_runner
.text_pt
1349 def escapestring(s
, replace
={" ": "~",
1361 "\\": "{$\setminus$}",
1363 "escape all ascii characters such that they are printable by TeX/LaTeX"
1366 if not 32 <= ord(s
[i
]) < 127:
1367 raise ValueError("escapestring function handles ascii strings only")
1374 s
= s
[:i
] + r
+ s
[i
+1:]