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")
31 def indent_text(text
):
32 "Prepends two spaces to each line in text."
33 return "".join(" " + line
for line
in text
.splitlines(True))
36 def remove_string(p
, s
):
37 """Removes a string from a string.
39 The function removes the first occurrence of a string in another string.
41 :param str p: string to be removed
42 :param str s: string to be searched
43 :returns: tuple of the result string and a success boolean (``True`` when
44 the string was removed)
45 :rtype: tuple of str and bool
48 >>> remove_string("XXX", "abcXXXdefXXXghi")
49 ('abcdefXXXghi', True)
52 r
= s
.replace(p
, '', 1)
56 def remove_pattern(p
, s
, ignore_nl
=True):
57 r
"""Removes a pattern from a string.
59 The function removes the first occurence of the pattern from a string. It
60 returns a tuple of the resulting string and the matching object (or
61 ``None``, if the pattern did not match).
63 :param re.regex p: pattern to be removed
64 :param str s: string to be searched
65 :param bool ignore_nl: When ``True``, newlines in the string are ignored
66 during the pattern search. The returned string will still contain all
67 newline characters outside of the matching part of the string, whereas
68 the returned matching object will not contain the newline characters
69 inside of the matching part of the string.
70 :returns: the result string and the match object or ``None`` if
72 :rtype: tuple of str and (re.match or None)
75 >>> r, m = remove_pattern(re.compile("XXX"), 'ab\ncXX\nXdefXX\nX')
78 >>> m.string[m.start():m.end()]
83 r
= s
.replace('\n', '')
90 s_start
= r_start
= m
.start()
91 s_end
= r_end
= m
.end()
102 return s
[:s_start
] + s
[s_end
:], m
107 """Return list of positions of a character in a string.
110 >>> index_all("X", "abXcdXef")
115 return [i
for i
, x
in enumerate(s
) if x
== c
]
119 """Returns iterator over pairs of data from an iterable.
122 >>> list(pairwise([1, 2, 3]))
126 a
, b
= itertools
.tee(i
)
131 def remove_nested_brackets(s
, openbracket
="(", closebracket
=")", quote
='"'):
132 """Remove nested brackets
134 Return a modified string with all nested brackets 1 removed, i.e. only
135 keep the first bracket nesting level. In case an opening bracket is
136 immediately followed by a quote, the quoted string is left untouched,
137 even if it contains brackets. The use-case for that are files in the
138 folder "Program Files (x86)".
140 If the bracket nesting level is broken (unbalanced), the unmodified
144 >>> remove_nested_brackets('aaa("bb()bb" cc(dd(ee))ff)ggg'*2)
145 'aaa("bb()bb" ccff)gggaaa("bb()bb" ccff)ggg'
148 openpos
= index_all(openbracket
, s
)
149 closepos
= index_all(closebracket
, s
)
150 if quote
is not None:
151 quotepos
= index_all(quote
, s
)
152 for openquote
, closequote
in pairwise(quotepos
):
153 if openquote
-1 in openpos
:
154 # ignore brackets in quoted string
155 openpos
= [pos
for pos
in openpos
156 if not (openquote
< pos
< closequote
)]
157 closepos
= [pos
for pos
in closepos
158 if not (openquote
< pos
< closequote
)]
159 if len(openpos
) != len(closepos
):
160 # unbalanced brackets
163 # keep the original string in case we need to return due to broken nesting levels
167 # Iterate over the bracket positions from the end.
168 # We go reversely to be able to immediately remove nested bracket levels
169 # without influencing bracket positions yet to come in the loop.
170 for pos
, leveldelta
in sorted(itertools
.chain(zip(openpos
, itertools
.repeat(-1)),
171 zip(closepos
, itertools
.repeat(1))),
173 # the current bracket nesting level
176 # unbalanced brackets
178 if leveldelta
== 1 and level
== 2:
179 # a closing bracket to cut after
181 if leveldelta
== -1 and level
== 1:
182 # an opening bracket to cut at -> remove
183 r
= r
[:pos
] + r
[endpos
:]
187 class TexResultError(ValueError):
188 "Error raised by :class:`texmessage` parsers."
193 """Collection of TeX output parsers.
195 This class is not meant to be instanciated. Instead, it serves as a
196 namespace for TeX output parsers, which are functions receiving a TeX
197 output and returning parsed output.
199 In addition, this class also contains some generator functions (namely
200 :attr:`texmessage.no_file` and :attr:`texmessage.pattern`), which return a
201 function according to the given parameters. They are used to generate some
202 of the parsers in this class and can be used to create others as well.
205 start_pattern
= re
.compile(r
"This is [-0-9a-zA-Z\s_]*TeX")
209 r
"""Validate TeX/LaTeX startup message including scrollmode test.
212 >>> texmessage.start(r'''
213 ... This is e-TeX (version)
214 ... *! Undefined control sequence.
221 # check for "This is e-TeX" etc.
222 if not texmessage
.start_pattern
.search(msg
):
223 raise TexResultError("TeX startup failed")
225 # check for \raiseerror -- just to be sure that communication works
226 new
= msg
.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[-1]
228 raise TexResultError("TeX scrollmode check failed")
232 def no_file(fileending
, qualname
=None):
233 "Generator function to ignore the missing file message for fileending."
235 "Ignore the missing {} file message."
236 return msg
.replace("No file texput.%s." % fileending
, "").replace("No file %s%stexput.%s." % (os
.curdir
, os
.sep
, fileending
), "")
237 check
.__doc
__ = check
.__doc
__.format(fileending
)
238 if qualname
is not None:
239 check
.__qualname
__ = qualname
242 no_aux
= staticmethod(no_file
.__func
__("aux", "texmessage.no_aux"))
243 no_nav
= staticmethod(no_file
.__func
__("nav", "texmessage.no_nav"))
245 aux_pattern
= re
.compile(r
'\(([^()]+\.aux|"[^"]+\.aux")\)')
246 log_pattern
= re
.compile(r
"Transcript written on .*texput\.log\.", re
.DOTALL
)
250 "Validate TeX shutdown message."
251 msg
= re
.sub(texmessage
.aux_pattern
, "", msg
).replace("(see the transcript file for additional information)", "")
253 # check for "Transcript written on ...log."
254 msg
, m
= remove_pattern(texmessage
.log_pattern
, msg
)
256 raise TexResultError("TeX logfile message expected")
259 quoted_file_pattern
= re
.compile(r
'\("(?P<filename>[^"]+)".*?\)')
260 file_pattern
= re
.compile(r
'\((?P<filename>[^"][^ )]*).*?\)', re
.DOTALL
)
264 """Ignore file loading messages.
266 Removes text starting with a round bracket followed by a filename
267 ignoring all further text until the corresponding closing bracket.
268 Quotes and/or line breaks in the filename are handled as needed for TeX
271 Without quoting the filename, the necessary removal of line breaks is
272 not well defined and the different possibilities are tested to check
273 whether one solution is ok. The last of the examples below checks this
277 >>> texmessage.load(r'''other (text.py) things''')
279 >>> texmessage.load(r'''other ("text.py") things''')
281 >>> texmessage.load(r'''other ("tex
282 ... t.py" further (ignored)
285 >>> texmessage.load(r'''other (t
289 ... ther (ignored) text) things''')
293 r
= remove_nested_brackets(msg
)
294 r
, m
= remove_pattern(texmessage
.quoted_file_pattern
, r
)
296 if not os
.path
.isfile(config
.get("text", "chroot", "") + m
.group("filename")):
298 r
, m
= remove_pattern(texmessage
.quoted_file_pattern
, r
)
299 r
, m
= remove_pattern(texmessage
.file_pattern
, r
, ignore_nl
=False)
301 for filename
in itertools
.accumulate(m
.group("filename").split("\n")):
302 if os
.path
.isfile(config
.get("text", "chroot", "") + filename
):
306 r
, m
= remove_pattern(texmessage
.file_pattern
, r
, ignore_nl
=False)
309 quoted_def_pattern
= re
.compile(r
'\("(?P<filename>[^"]+\.(fd|def))"\)')
310 def_pattern
= re
.compile(r
'\((?P<filename>[^"][^ )]*\.(fd|def))\)')
314 "Ignore font definition (``*.fd`` and ``*.def``) loading messages."
316 for p
in [texmessage
.quoted_def_pattern
, texmessage
.def_pattern
]:
317 r
, m
= remove_pattern(p
, r
)
319 if not os
.path
.isfile(config
.get("text", "chroot", "") + m
.group("filename")):
321 r
, m
= remove_pattern(p
, r
)
324 quoted_graphics_pattern
= re
.compile(r
'<"(?P<filename>[^"]+\.eps)">')
325 graphics_pattern
= re
.compile(r
'<(?P<filename>[^"][^>]*\.eps)>')
328 def load_graphics(msg
):
329 "Ignore graphics file (``*.eps``) loading messages."
331 for p
in [texmessage
.quoted_graphics_pattern
, texmessage
.graphics_pattern
]:
332 r
, m
= remove_pattern(p
, r
)
334 if not os
.path
.isfile(config
.get("text", "chroot", "") + m
.group("filename")):
336 r
, m
= remove_pattern(texmessage
.quoted_file_pattern
, r
)
341 """Ignore all messages.
343 Should be used as a last resort only. You should write a proper TeX
344 output parser function for the output you observe.
351 """Warn about all messages.
353 Similar to :attr:`ignore`, but writing a warning to the logger about
354 the TeX output. This is considered to be better when you need to get it
355 working quickly as you will still be prompted about the unresolved
356 output, while the processing continues.
360 logger
.warning("ignoring TeX warnings:\n%s" % indent_text(msg
.rstrip()))
364 def pattern(p
, warning
, qualname
=None):
365 "Warn by regular expression pattern matching."
368 msg
, m
= remove_pattern(p
, msg
, ignore_nl
=False)
370 logger
.warning("ignoring %s:\n%s" % (warning
, m
.string
[m
.start(): m
.end()].rstrip()))
371 msg
, m
= remove_pattern(p
, msg
, ignore_nl
=False)
373 check
.__doc
__ = check
.__doc
__.format(warning
)
374 if qualname
is not None:
375 check
.__qualname
__ = qualname
378 box_warning
= staticmethod(pattern
.__func
__(re
.compile(r
"^(Overfull|Underfull) \\[hv]box.*$(\n^..*$)*\n^$\n", re
.MULTILINE
),
379 "overfull/underfull box", qualname
="texmessage.box_warning"))
380 font_warning
= staticmethod(pattern
.__func
__(re
.compile(r
"^LaTeX Font Warning: .*$(\n^\(Font\).*$)*", re
.MULTILINE
),
381 "font substitutions of NFSS", qualname
="texmessage.font_warning"))
382 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
),
383 "generic package messages", qualname
="texmessage.package_warning"))
384 rerun_warning
= staticmethod(pattern
.__func
__(re
.compile(r
"^(LaTeX Warning: Label\(s\) may have changed\. Rerun to get cross-references right\s*\.)$", re
.MULTILINE
),
385 "rerun required message", qualname
="texmessage.rerun_warning"))
386 nobbl_warning
= staticmethod(pattern
.__func
__(re
.compile(r
"^[\s\*]*(No file .*\.bbl.)\s*", re
.MULTILINE
),
387 "no-bbl message", qualname
="texmessage.nobbl_warning"))
390 ###############################################################################
392 ###############################################################################
394 _textattrspreamble
= ""
397 "a textattr defines a apply method, which modifies a (La)TeX expression"
399 class _localattr
: pass
401 _textattrspreamble
+= r
"""\gdef\PyXFlushHAlign{0}%
403 \leftskip=0pt plus \PyXFlushHAlign fil%
404 \rightskip=0pt plus 1fil%
405 \advance\rightskip0pt plus -\PyXFlushHAlign fil%
411 \exhyphenpenalty=9999}%
414 class boxhalign(attr
.exclusiveattr
, textattr
, _localattr
):
416 def __init__(self
, aboxhalign
):
417 self
.boxhalign
= aboxhalign
418 attr
.exclusiveattr
.__init
__(self
, boxhalign
)
420 def apply(self
, expr
):
421 return r
"\gdef\PyXBoxHAlign{%.5f}%s" % (self
.boxhalign
, expr
)
423 boxhalign
.left
= boxhalign(0)
424 boxhalign
.center
= boxhalign(0.5)
425 boxhalign
.right
= boxhalign(1)
426 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass for boxhalign since it can't clear a halign's boxhalign
429 class flushhalign(attr
.exclusiveattr
, textattr
, _localattr
):
431 def __init__(self
, aflushhalign
):
432 self
.flushhalign
= aflushhalign
433 attr
.exclusiveattr
.__init
__(self
, flushhalign
)
435 def apply(self
, expr
):
436 return r
"\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self
.flushhalign
, expr
)
438 flushhalign
.left
= flushhalign(0)
439 flushhalign
.center
= flushhalign(0.5)
440 flushhalign
.right
= flushhalign(1)
441 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
444 class halign(boxhalign
, flushhalign
, _localattr
):
446 def __init__(self
, aboxhalign
, aflushhalign
):
447 self
.boxhalign
= aboxhalign
448 self
.flushhalign
= aflushhalign
449 attr
.exclusiveattr
.__init
__(self
, halign
)
451 def apply(self
, expr
):
452 return r
"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self
.boxhalign
, self
.flushhalign
, expr
)
454 halign
.left
= halign(0, 0)
455 halign
.center
= halign(0.5, 0.5)
456 halign
.right
= halign(1, 1)
457 halign
.clear
= attr
.clearclass(halign
)
458 halign
.boxleft
= boxhalign
.left
459 halign
.boxcenter
= boxhalign
.center
460 halign
.boxright
= boxhalign
.right
461 halign
.flushleft
= halign
.raggedright
= flushhalign
.left
462 halign
.flushcenter
= halign
.raggedcenter
= flushhalign
.center
463 halign
.flushright
= halign
.raggedleft
= flushhalign
.right
466 class _mathmode(attr
.attr
, textattr
, _localattr
):
469 def apply(self
, expr
):
470 return r
"$\displaystyle{%s}$" % expr
472 mathmode
= _mathmode()
473 clearmathmode
= attr
.clearclass(_mathmode
)
476 class _phantom(attr
.attr
, textattr
, _localattr
):
479 def apply(self
, expr
):
480 return r
"\phantom{%s}" % expr
483 clearphantom
= attr
.clearclass(_phantom
)
486 _textattrspreamble
+= "\\newbox\\PyXBoxVBox%\n\\newdimen\\PyXDimenVBox%\n"
488 class parbox_pt(attr
.sortbeforeexclusiveattr
, textattr
):
494 def __init__(self
, width
, baseline
=top
):
495 self
.width
= width
* 72.27 / (unit
.scale
["x"] * 72)
496 self
.baseline
= baseline
497 attr
.sortbeforeexclusiveattr
.__init
__(self
, parbox_pt
, [_localattr
])
499 def apply(self
, expr
):
500 if self
.baseline
== self
.top
:
501 return r
"\linewidth=%.5ftruept\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self
.width
, expr
)
502 elif self
.baseline
== self
.middle
:
503 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
)
504 elif self
.baseline
== self
.bottom
:
505 return r
"\linewidth=%.5ftruept\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self
.width
, expr
)
507 ValueError("invalid baseline argument")
509 parbox_pt
.clear
= attr
.clearclass(parbox_pt
)
511 class parbox(parbox_pt
):
513 def __init__(self
, width
, **kwargs
):
514 parbox_pt
.__init
__(self
, unit
.topt(width
), **kwargs
)
516 parbox
.clear
= parbox_pt
.clear
519 _textattrspreamble
+= "\\newbox\\PyXBoxVAlign%\n\\newdimen\\PyXDimenVAlign%\n"
521 class valign(attr
.sortbeforeexclusiveattr
, textattr
):
523 def __init__(self
, avalign
):
524 self
.valign
= avalign
525 attr
.sortbeforeexclusiveattr
.__init
__(self
, valign
, [parbox_pt
, _localattr
])
527 def apply(self
, expr
):
528 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
)
530 valign
.top
= valign(0)
531 valign
.middle
= valign(0.5)
532 valign
.bottom
= valign(1)
533 valign
.clear
= valign
.baseline
= attr
.clearclass(valign
)
536 _textattrspreamble
+= "\\newdimen\\PyXDimenVShift%\n"
538 class _vshift(attr
.sortbeforeattr
, textattr
):
541 attr
.sortbeforeattr
.__init
__(self
, [valign
, parbox_pt
, _localattr
])
543 def apply(self
, expr
):
544 return r
"%s\setbox0\hbox{{%s}}\lower\PyXDimenVShift\box0" % (self
.setheightexpr(), expr
)
546 class vshift(_vshift
):
547 "vertical down shift by a fraction of a character height"
549 def __init__(self
, lowerratio
, heightstr
="0"):
550 _vshift
.__init
__(self
)
551 self
.lowerratio
= lowerratio
552 self
.heightstr
= heightstr
554 def setheightexpr(self
):
555 return r
"\setbox0\hbox{{%s}}\PyXDimenVShift=%.5f\ht0" % (self
.heightstr
, self
.lowerratio
)
557 class _vshiftmathaxis(_vshift
):
558 "vertical down shift by the height of the math axis"
560 def setheightexpr(self
):
561 return r
"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\PyXDimenVShift=\ht0"
564 vshift
.bottomzero
= vshift(0)
565 vshift
.middlezero
= vshift(0.5)
566 vshift
.topzero
= vshift(1)
567 vshift
.mathaxis
= _vshiftmathaxis()
568 vshift
.clear
= attr
.clearclass(_vshift
)
571 defaultsizelist
= ["normalsize", "large", "Large", "LARGE", "huge", "Huge",
572 None, "tiny", "scriptsize", "footnotesize", "small"]
574 class size(attr
.sortbeforeattr
, textattr
):
577 def __init__(self
, sizeindex
=None, sizename
=None, sizelist
=defaultsizelist
):
578 if (sizeindex
is None and sizename
is None) or (sizeindex
is not None and sizename
is not None):
579 raise ValueError("either specify sizeindex or sizename")
580 attr
.sortbeforeattr
.__init
__(self
, [_mathmode
, _vshift
])
581 if sizeindex
is not None:
582 if sizeindex
>= 0 and sizeindex
< sizelist
.index(None):
583 self
.size
= sizelist
[sizeindex
]
584 elif sizeindex
< 0 and sizeindex
+ len(sizelist
) > sizelist
.index(None):
585 self
.size
= sizelist
[sizeindex
]
587 raise IndexError("index out of sizelist range")
591 def apply(self
, expr
):
592 return r
"\%s{}%s" % (self
.size
, expr
)
595 size
.scriptsize
= size
.script
= size(-3)
596 size
.footnotesize
= size
.footnote
= size(-2)
597 size
.small
= size(-1)
598 size
.normalsize
= size
.normal
= size(0)
604 size
.clear
= attr
.clearclass(size
)
607 ###############################################################################
609 ###############################################################################
612 class MonitorOutput(threading
.Thread
):
614 def __init__(self
, name
, output
):
615 """Deadlock-safe output stream reader and monitor.
617 An instance of this class creates a thread to continously read lines
618 from a stream. By that a deadlock due to a full pipe is prevented. In
619 addition, the stream content can be monitored for containing a certain
620 string (see :meth:`expect` and :meth:`wait`) and return all the
621 collected output (see :meth:`read`).
623 :param string name: name to be used while logging in :meth:`wait` and
625 :param file output: output stream
629 self
._expect
= queue
.Queue(1)
630 self
._received
= threading
.Event()
631 self
._output
= queue
.Queue()
632 threading
.Thread
.__init
__(self
, name
=name
)
637 """Expect a string on a **single** line in the output.
639 This method must be called **before** the output occurs, i.e. before
640 the input is written to the TeX/LaTeX process.
642 :param s: expected string or ``None`` if output is expected to become
647 self
._expect
.put_nowait(s
)
650 """Read all output collected since its previous call.
652 The output reading should be synchronized by the :meth:`expect`
653 and :meth:`wait` methods.
655 :returns: collected output from the stream
662 l
.append(self
._output
.get_nowait())
665 return "".join(l
).replace("\r\n", "\n").replace("\r", "\n")
667 def _wait(self
, waiter
, checker
):
668 """Helper method to implement :meth:`wait` and :meth:`done`.
670 Waits for an event using the *waiter* and *checker* functions while
671 providing user feedback to the ``pyx``-logger using the warning level
672 according to the ``wait`` and ``showwait`` from the ``text`` section of
673 the pyx :mod:`config`.
675 :param function waiter: callback to wait for (the function gets called
676 with a timeout parameter)
677 :param function checker: callback returing ``True`` if
678 waiting was successful
679 :returns: ``True`` when wait was successful
683 wait
= config
.getint("text", "wait", 60)
684 showwait
= config
.getint("text", "showwait", 5)
688 while waited
< wait
and not hasevent
:
689 if wait
- waited
> showwait
:
693 waiter(wait
- waited
)
694 waited
+= wait
- waited
698 logger
.warning("Still waiting for {} "
699 "after {} (of {}) seconds..."
700 .format(self
.name
, waited
, wait
))
702 logger
.warning("The timeout of {} seconds expired "
703 "and {} did not respond."
704 .format(waited
, self
.name
))
711 """Wait for the expected output to happen.
713 Waits either until a line containing the string set by the previous
714 :meth:`expect` call is found, or a timeout occurs.
716 :returns: ``True`` when the expected string was found
720 r
= self
._wait
(self
._received
.wait
, self
._received
.isSet
)
722 self
._received
.clear()
726 """Waits until the output becomes empty.
728 Waits either until the output becomes empty, or a timeout occurs.
729 The generated output can still be catched by :meth:`read` after
730 :meth:`done` was successful.
732 In the proper workflow :meth:`expect` should be called with ``None``
733 before the output completes, as otherwise a ``ValueError`` is raised
736 :returns: ``True`` when the output has become empty
740 return self
._wait
(self
.join
, lambda self
=self
: not self
.is_alive())
743 """Read a line from the output.
745 To be used **inside** the thread routine only.
747 :returns: one line of the output as a string
753 return self
.output
.readline()
755 if e
.errno
!= errno
.EINTR
:
761 **Not** to be called from outside.
763 :raises ValueError: output becomes empty while some string is expected
768 line
= self
._readline
()
771 expect
= self
._expect
.get_nowait()
776 self
._output
.put(line
)
777 if expect
is not None:
778 found
= line
.find(expect
)
783 if expect
is not None:
784 raise ValueError("{} finished unexpectedly".format(self
.name
))
787 class textbox_pt(box
.rect
, baseclasses
.canvasitem
):
790 def __init__(self
, x_pt
, y_pt
, left_pt
, right_pt
, height_pt
, depth_pt
, do_finish
, fontmap
, singlecharmode
, fillstyles
):
793 An instance of this class contains the text output generated by PyX. It
794 is a :class:`baseclasses.canvasitem` and thus can be inserted into a
797 .. A text has a center (x_pt, y_pt) as well as extents in x-direction
798 .. (left_pt and right_pt) and y-direction (hight_pt and depth_pt). The
799 .. textbox positions the output accordingly and scales it by the
800 .. x-scale from the :mod:`unit`.
802 .. :param float x_pt: x coordinate of the center in pts
803 .. :param float y_pt: y coordinate of the center in pts
804 .. :param float left_pt: unscaled left extent in pts
805 .. :param float right_pt: unscaled right extent in pts
806 .. :param float height_pt: unscaled height in pts
807 .. :param float depth_pt: unscaled depth in pts
808 .. :param function do_finish: callable to execute :meth:`readdvipage`
809 .. :param fontmap: force a fontmap to be used (instead of the default
810 .. depending on the output format)
811 .. :type fontmap: None or fontmap
812 .. :param bool singlecharmode: position each character separately
813 .. :param fillstyles: fill styles to be applied
814 .. :type fillstyles: list of fillstyles
817 self
.left
= left_pt
*unit
.x_pt
#: left extent of the text (PyX length)
818 self
.right
= right_pt
*unit
.x_pt
#: right extent of the text (PyX length)
819 self
.width
= self
.left
+ self
.right
#: width of the text (PyX length)
820 self
.height
= height_pt
*unit
.x_pt
#: height of the text (PyX length)
821 self
.depth
= depth_pt
*unit
.x_pt
#: height of the text (PyX length)
823 self
.do_finish
= do_finish
824 self
.fontmap
= fontmap
825 self
.singlecharmode
= singlecharmode
826 self
.fillstyles
= fillstyles
828 self
.texttrafo
= trafo
.scale(unit
.scale
["x"]).translated_pt(x_pt
, y_pt
)
829 box
.rect_pt
.__init
__(self
, x_pt
- left_pt
*unit
.scale
["x"], y_pt
- depth_pt
*unit
.scale
["x"],
830 (left_pt
+ right_pt
)*unit
.scale
["x"],
831 (depth_pt
+ height_pt
)*unit
.scale
["x"],
832 abscenter_pt
= (left_pt
*unit
.scale
["x"], depth_pt
*unit
.scale
["x"]))
834 self
._dvicanvas
= None
836 def transform(self
, *trafos
):
837 box
.rect
.transform(self
, *trafos
)
839 self
.texttrafo
= trafo
* self
.texttrafo
840 if self
._dvicanvas
is not None:
842 self
._dvicanvas
.trafo
= trafo
* self
._dvicanvas
.trafo
844 def readdvipage(self
, dvifile
, page
):
845 self
._dvicanvas
= dvifile
.readpage([ord("P"), ord("y"), ord("X"), page
, 0, 0, 0, 0, 0, 0],
846 fontmap
=self
.fontmap
, singlecharmode
=self
.singlecharmode
, attrs
=[self
.texttrafo
] + self
.fillstyles
)
850 if self
._dvicanvas
is None:
852 return self
._dvicanvas
854 def marker(self
, name
):
855 """Return the position of a marker.
857 :param str name: name of the marker
858 :returns: position of the marker
859 :rtype: tuple of two PyX lengths
861 This method returns the position of the marker of the given name
862 within, previously defined by the ``\\PyXMarker{name}`` macro in the
863 typeset text. Please do not use the ``@`` character within your name to
864 prevent name clashes with PyX internal uses (although we don’t the
865 marker feature internally right now).
867 Similar to generating actual output, the marker function accesses the
868 DVI output, stopping. The :ref:`texipc` feature will allow for this access
869 without stopping the TeX interpreter.
872 return self
.texttrafo
.apply(*self
.dvicanvas
.markers
[name
])
875 textpath
= path
.path()
876 for item
in self
.dvicanvas
.items
:
877 textpath
+= item
.textpath()
878 return textpath
.transformed(self
.texttrafo
)
880 def processPS(self
, file, writer
, context
, registry
, bbox
):
881 abbox
= bboxmodule
.empty()
882 self
.dvicanvas
.processPS(file, writer
, context
, registry
, abbox
)
883 bbox
+= box
.rect
.bbox(self
)
885 def processPDF(self
, file, writer
, context
, registry
, bbox
):
886 abbox
= bboxmodule
.empty()
887 self
.dvicanvas
.processPDF(file, writer
, context
, registry
, abbox
)
888 bbox
+= box
.rect
.bbox(self
)
890 def processSVG(self
, xml
, writer
, context
, registry
, bbox
):
891 abbox
= bboxmodule
.empty()
892 self
.dvicanvas
.processSVG(xml
, writer
, context
, registry
, abbox
)
893 bbox
+= box
.rect
.bbox(self
)
901 "Constants defining the verbosity of the :exc:`TexResultError`."
902 none
= 0 #: Without any input and output.
903 default
= 1 #: Input and parsed output shortend to 5 lines.
904 full
= 2 #: Full input and unparsed as well as parsed output.
909 def __init__(self
, *files
):
910 """Apply write, flush, and close to each of the given files."""
913 def write(self
, data
):
914 for file in self
.files
:
918 for file in self
.files
:
922 for file in self
.files
:
925 # The texrunner state represents the next (or current) execute state.
926 STATE_START
, STATE_PREAMBLE
, STATE_TYPESET
, STATE_DONE
= range(4)
927 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:")
928 dvi_pattern
= re
.compile(r
"Output written on .*texput\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\.", re
.DOTALL
)
930 class TexDoneError(Exception):
936 #: default :class:`texmessage` parsers at interpreter startup
937 texmessages_start_default
= [texmessage
.start
]
938 #: default :class:`texmessage` parsers at interpreter shutdown
939 texmessages_end_default
= [texmessage
.end
, texmessage
.font_warning
, texmessage
.rerun_warning
, texmessage
.nobbl_warning
]
940 #: default :class:`texmessage` parsers for preamble output
941 texmessages_preamble_default
= [texmessage
.load
]
942 #: default :class:`texmessage` parsers for typeset output
943 texmessages_run_default
= [texmessage
.font_warning
, texmessage
.box_warning
, texmessage
.package_warning
,
944 texmessage
.load_def
, texmessage
.load_graphics
]
946 def __init__(self
, cmd
,
949 texipc
=config
.getboolean("text", "texipc", 0),
952 errordetail
=errordetail
.default
,
953 texmessages_start
=[],
955 texmessages_preamble
=[],
957 """Base class for the TeX interface.
959 .. note:: This class cannot be used directly. It is the base class for
960 all texrunners and provides most of the implementation.
961 Still, to the end user the parameters except for *cmd*
962 are important, as they are preserved in derived classes
965 :param cmd: command and arguments to start the TeX interpreter
966 :type cmd: list of str
967 :param str texenc: encoding to use in the communication with the TeX
969 :param usefiles: list of supplementary files to be copied to and from
970 the temporary working directory (see :ref:`debug` for usage
972 :type usefiles: list of str
973 :param bool texipc: :ref:`texipc` flag.
974 :param copyinput: filename or file to be used to store a copy of all
975 the input passed to the TeX interpreter
976 :type copyinput: None or str or file
977 :param bool dvitype: flag to turn on dvitype-like output
978 :param errordetail: verbosity of the :exc:`TexResultError`
979 :type errordetail: :class:`errordetail`
980 :param texmessages_start: additional message parsers at interpreter
982 :type texmessages_start: list of :class:`texmessage` parsers
983 :param texmessages_end: additional message parsers at interpreter
985 :type texmessages_end: list of :class:`texmessage` parsers
986 :param texmessages_preamble: additional message parsers for preamble
988 :type texmessages_preamble: list of :class:`texmessage` parsers
989 :param texmessages_run: additional message parsers for typset output
990 :type texmessages_run: list of :class:`texmessage` parsers
995 self
.usefiles
= usefiles
997 self
.copyinput
= copyinput
998 self
.dvitype
= dvitype
999 self
.errordetail
= errordetail
1000 self
.texmessages_start
= texmessages_start
1001 self
.texmessages_end
= texmessages_end
1002 self
.texmessages_preamble
= texmessages_preamble
1003 self
.texmessages_run
= texmessages_run
1005 self
.state
= STATE_START
1009 self
.needdvitextboxes
= [] # when texipc-mode off
1013 """Clean-up TeX interpreter and tmp directory.
1015 This funtion is hooked up in atexit to quit the TeX interpreter, to
1016 save the contents of usefiles, and to remove the temporary directory.
1020 if self
.state
> STATE_START
:
1021 if self
.state
< STATE_DONE
:
1023 if self
.state
< STATE_DONE
: # cleanup while TeX is still running?
1024 self
.texoutput
.expect(None)
1026 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
)),
1027 (self
.popen
.terminate
, "Failed, too. Trying by kill signal now ..."),
1028 (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
))]:
1030 if self
.texoutput
.done():
1033 for usefile
in self
.usefiles
:
1034 extpos
= usefile
.rfind(".")
1036 os
.rename(os
.path
.join(self
.tmpdir
, "texput" + usefile
[extpos
:]), usefile
)
1037 except EnvironmentError:
1038 logger
.warning("Could not save '{}'.".format(usefile
))
1039 if os
.path
.isfile(usefile
):
1042 except EnvironmentError:
1043 logger
.warning("Failed to remove spurious file '{}'.".format(usefile
))
1045 shutil
.rmtree(self
.tmpdir
, ignore_errors
=True)
1047 def _execute(self
, expr
, texmessages
, oldstate
, newstate
):
1048 """Execute TeX expression.
1050 :param str expr: expression to be passed to TeX
1051 :param texmessages: message parsers to analyse the textual output of
1053 :type texmessages: list of :class:`texmessage` parsers
1054 :param int oldstate: state of the TeX interpreter prior to the
1055 expression execution
1056 :param int newstate: state of the TeX interpreter after to the
1057 expression execution
1060 assert STATE_PREAMBLE
<= oldstate
<= STATE_TYPESET
1061 assert oldstate
== self
.state
1062 assert newstate
>= oldstate
1063 if newstate
== STATE_DONE
:
1064 self
.texoutput
.expect(None)
1065 self
.texinput
.write(expr
)
1068 # test to encode expr early to not pile up expected results
1069 # if the expression won't make it to the texinput at all
1070 # (which would otherwise harm a proper cleanup)
1071 expr
.encode(self
.texenc
)
1073 if oldstate
== newstate
== STATE_TYPESET
:
1075 expr
= "\\ProcessPyXBox{%s%%\n}{%i}" % (expr
, self
.page
)
1077 self
.texoutput
.expect("PyXInputMarker:executeid=%i:" % self
.executeid
)
1078 expr
+= "%%\n\\PyXInput{%i}%%\n" % self
.executeid
1079 self
.texinput
.write(expr
)
1080 self
.texinput
.flush()
1081 self
.state
= newstate
1082 if newstate
== STATE_DONE
:
1083 wait_ok
= self
.texoutput
.done()
1085 wait_ok
= self
.texoutput
.wait()
1087 parsed
= unparsed
= self
.texoutput
.read()
1089 raise TexResultError("TeX didn't respond as expected within the timeout period.")
1090 if newstate
!= STATE_DONE
:
1091 parsed
, m
= remove_string("PyXInputMarker:executeid=%s:" % self
.executeid
, parsed
)
1093 raise TexResultError("PyXInputMarker expected")
1094 if oldstate
== newstate
== STATE_TYPESET
:
1095 parsed
, m
= remove_pattern(PyXBoxPattern
, parsed
, ignore_nl
=False)
1097 raise TexResultError("PyXBox expected")
1098 if m
.group("page") != str(self
.page
):
1099 raise TexResultError("Wrong page number in PyXBox")
1100 extent_pt
= [float(x
)*72/72.27 for x
in m
.group("lt", "rt", "ht", "dp")]
1101 parsed
, m
= remove_string("[80.121.88.%s]" % self
.page
, parsed
)
1103 raise TexResultError("PyXPageOutMarker expected")
1105 # check for "Output written on ...dvi (1 page, 220 bytes)."
1107 parsed
, m
= remove_pattern(dvi_pattern
, parsed
)
1109 raise TexResultError("TeX dvifile messages expected")
1110 if m
.group("page") != str(self
.page
):
1111 raise TexResultError("wrong number of pages reported")
1113 parsed
, m
= remove_string("No pages of output.", parsed
)
1115 raise TexResultError("no dvifile expected")
1117 for t
in texmessages
:
1119 if parsed
.replace(r
"(Please type a command or say `\end')", "").replace(" ", "").replace("*\n", "").replace("\n", ""):
1120 raise TexResultError("unhandled TeX response (might be an error)")
1121 except TexResultError
as e
:
1122 if self
.errordetail
> errordetail
.none
:
1123 def add(msg
): e
.args
= (e
.args
[0] + msg
,)
1124 add("\nThe expression passed to TeX was:\n{}".format(indent_text(expr
.rstrip())))
1125 if self
.errordetail
== errordetail
.full
:
1126 add("\nThe return message from TeX was:\n{}".format(indent_text(unparsed
.rstrip())))
1127 if self
.errordetail
== errordetail
.default
:
1128 if parsed
.count('\n') > 6:
1129 parsed
= "\n".join(parsed
.split("\n")[:5] + ["(cut after 5 lines; use errordetail.full for all output)"])
1130 add("\nAfter parsing the return message from TeX, the following was left:\n{}".format(indent_text(parsed
.rstrip())))
1132 if oldstate
== newstate
== STATE_TYPESET
:
1136 """Setup environment and start TeX interpreter."""
1137 assert self
.state
== STATE_START
1138 self
.state
= STATE_PREAMBLE
1140 chroot
= config
.get("text", "chroot", "")
1142 chroot_tmpdir
= config
.get("text", "tmpdir", "/tmp")
1143 chroot_tmpdir_rel
= os
.path
.relpath(chroot_tmpdir
, os
.sep
)
1144 base_tmpdir
= os
.path
.join(chroot
, chroot_tmpdir_rel
)
1146 base_tmpdir
= config
.get("text", "tmpdir", None)
1147 self
.tmpdir
= tempfile
.mkdtemp(prefix
="pyx", dir=base_tmpdir
)
1148 atexit
.register(self
._cleanup
)
1149 for usefile
in self
.usefiles
:
1150 extpos
= usefile
.rfind(".")
1152 os
.rename(usefile
, os
.path
.join(self
.tmpdir
, "texput" + usefile
[extpos
:]))
1156 tex_tmpdir
= os
.sep
+ os
.path
.relpath(self
.tmpdir
, chroot
)
1158 tex_tmpdir
= self
.tmpdir
1159 cmd
= self
.cmd
+ ['--output-directory', tex_tmpdir
]
1162 self
.popen
= config
.Popen(cmd
, stdin
=config
.PIPE
, stdout
=config
.PIPE
, stderr
=config
.STDOUT
, bufsize
=0)
1163 self
.texinput
= io
.TextIOWrapper(self
.popen
.stdin
, encoding
=self
.texenc
)
1166 self
.copyinput
.write
1167 except AttributeError:
1168 self
.texinput
= Tee(open(self
.copyinput
, "w", encoding
=self
.texenc
), self
.texinput
)
1170 self
.texinput
= Tee(self
.copyinput
, self
.texinput
)
1171 self
.texoutput
= MonitorOutput(self
.name
, io
.TextIOWrapper(self
.popen
.stdout
, encoding
=self
.texenc
))
1172 self
._execute
("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
1173 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
1174 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
1175 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
1176 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
1177 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
1178 "\\newdimen\\PyXDimenHAlignRT%\n" +
1179 _textattrspreamble
+ # insert preambles for textattrs macros
1180 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
1181 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
1182 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
1183 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
1184 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
1185 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
1186 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
1187 "lt=\\the\\PyXDimenHAlignLT,"
1188 "rt=\\the\\PyXDimenHAlignRT,"
1189 "ht=\\the\\ht\\PyXBox,"
1190 "dp=\\the\\dp\\PyXBox:}%\n"
1191 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
1192 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
1193 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
1194 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
1195 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%", # write PyXMarker special into the dvi-file
1196 self
.texmessages_start_default
+ self
.texmessages_start
, STATE_PREAMBLE
, STATE_PREAMBLE
)
1198 def do_preamble(self
, expr
, texmessages
):
1199 """Ensure preamble mode and execute expr."""
1200 if self
.state
< STATE_PREAMBLE
:
1202 self
._execute
(expr
, texmessages
, STATE_PREAMBLE
, STATE_PREAMBLE
)
1204 def do_typeset(self
, expr
, texmessages
):
1205 """Ensure typeset mode and typeset expr."""
1206 if self
.state
< STATE_PREAMBLE
:
1208 if self
.state
< STATE_TYPESET
:
1210 return self
._execute
(expr
, texmessages
, STATE_TYPESET
, STATE_TYPESET
)
1212 def do_finish(self
):
1213 """Teardown TeX interpreter and cleanup environment."""
1214 if self
.state
== STATE_DONE
:
1216 if self
.state
< STATE_TYPESET
:
1219 assert self
.state
== STATE_DONE
1220 self
.texinput
.close() # close the input queue and
1221 self
.texoutput
.done() # wait for finish of the output
1223 if self
.needdvitextboxes
:
1224 dvifilename
= os
.path
.join(self
.tmpdir
, "texput.dvi")
1225 self
.dvifile
= dvifile
.DVIfile(dvifilename
, debug
=self
.dvitype
)
1227 for box
in self
.needdvitextboxes
:
1228 box
.readdvipage(self
.dvifile
, page
)
1230 if self
.dvifile
is not None and self
.dvifile
.readpage(None) is not None:
1231 raise ValueError("end of dvifile expected but further pages follow")
1233 atexit
.unregister(self
._cleanup
)
1236 def preamble(self
, expr
, texmessages
=[]):
1237 """Execute a preamble.
1239 :param str expr: expression to be executed
1240 :param texmessages: additional message parsers
1241 :type texmessages: list of :class:`texmessage` parsers
1243 Preambles must not generate output, but are used to load files, perform
1244 settings, define macros, *etc*. In LaTeX mode, preambles are executed
1245 before ``\\begin{document}``. The method can be called multiple times,
1246 but only prior to :meth:`SingleRunner.text` and
1247 :meth:`SingleRunner.text_pt`.
1250 texmessages
= self
.texmessages_preamble_default
+ self
.texmessages_preamble
+ texmessages
1251 self
.do_preamble(expr
, texmessages
)
1253 def text_pt(self
, x_pt
, y_pt
, expr
, textattrs
=[], texmessages
=[], fontmap
=None, singlecharmode
=False):
1256 :param float x_pt: x position in pts
1257 :param float y_pt: y position in pts
1258 :param str expr: text to be typeset
1259 :param textattrs: styles and attributes to be applied to the text
1260 :type textattrs: list of :class:`textattr, :class:`trafo.trafo_pt`,
1261 and :class:`style.fillstyle`
1262 :param texmessages: additional message parsers
1263 :type texmessages: list of :class:`texmessage` parsers
1264 :param fontmap: force a fontmap to be used (instead of the default
1265 depending on the output format)
1266 :type fontmap: None or fontmap
1267 :param bool singlecharmode: position each character separately
1268 :returns: text output insertable into a canvas.
1269 :rtype: :class:`textbox_pt`
1270 :raises: :exc:`TexDoneError`: when the TeX interpreter has been
1274 if self
.state
== STATE_DONE
:
1275 raise TexDoneError("typesetting process was terminated already")
1276 textattrs
= attr
.mergeattrs(textattrs
) # perform cleans
1277 attr
.checkattrs(textattrs
, [textattr
, trafo
.trafo_pt
, style
.fillstyle
])
1278 trafos
= attr
.getattrs(textattrs
, [trafo
.trafo_pt
])
1279 fillstyles
= attr
.getattrs(textattrs
, [style
.fillstyle
])
1280 textattrs
= attr
.getattrs(textattrs
, [textattr
])
1281 for ta
in textattrs
[::-1]:
1282 expr
= ta
.apply(expr
)
1283 first
= self
.state
< STATE_TYPESET
1284 left_pt
, right_pt
, height_pt
, depth_pt
= self
.do_typeset(expr
, self
.texmessages_run_default
+ self
.texmessages_run
+ texmessages
)
1285 if self
.texipc
and first
:
1286 self
.dvifile
= dvifile
.DVIfile(os
.path
.join(self
.tmpdir
, "texput.dvi"), debug
=self
.dvitype
)
1287 box
= textbox_pt(x_pt
, y_pt
, left_pt
, right_pt
, height_pt
, depth_pt
, self
.do_finish
, fontmap
, singlecharmode
, fillstyles
)
1289 box
.reltransform(t
) # TODO: should trafos really use reltransform???
1290 # this is quite different from what we do elsewhere!!!
1291 # see https://sourceforge.net/mailarchive/forum.php?thread_id=9137692&forum_id=23700
1293 box
.readdvipage(self
.dvifile
, self
.page
)
1295 self
.needdvitextboxes
.append(box
)
1298 def text(self
, x
, y
, *args
, **kwargs
):
1301 This method is identical to :meth:`text_pt` with the only difference of
1302 using PyX lengths to position the output.
1304 :param x: x position
1306 :param y: y position
1310 return self
.text_pt(unit
.topt(x
), unit
.topt(y
), *args
, **kwargs
)
1313 class SingleTexRunner(SingleRunner
):
1315 def __init__(self
, cmd
=config
.getlist("text", "tex", ["tex"]), lfs
="10pt", **kwargs
):
1316 """Plain TeX interface.
1318 This class adjusts the :class:`SingleRunner` to use plain TeX.
1320 :param cmd: command and arguments to start the TeX interpreter
1321 :type cmd: list of str
1322 :param lfs: resemble LaTeX font settings within plain TeX by loading a
1324 :type lfs: str or None
1325 :param kwargs: additional arguments passed to :class:`SingleRunner`
1327 An lfs-file is a file defining a set of font commands like ``\\normalsize``
1328 by font selection commands in plain TeX. Several of those files
1329 resembling standard settings of LaTeX are distributed along with PyX in
1330 the ``pyx/data/lfs`` directory. This directory also contains a LaTeX
1331 file to create lfs files for different settings (LaTeX class, class
1332 options, and style files).
1335 super().__init
__(cmd
=cmd
, **kwargs
)
1339 def go_typeset(self
):
1340 assert self
.state
== STATE_PREAMBLE
1341 self
.state
= STATE_TYPESET
1343 def go_finish(self
):
1344 self
._execute
("\\end%\n", self
.texmessages_end_default
+ self
.texmessages_end
, STATE_TYPESET
, STATE_DONE
)
1346 def force_done(self
):
1347 self
.texinput
.write("\n\\end\n")
1352 if not self
.lfs
.endswith(".lfs"):
1353 self
.lfs
= "%s.lfs" % self
.lfs
1354 with config
.open(self
.lfs
, []) as lfsfile
:
1355 lfsdef
= lfsfile
.read().decode("ascii")
1356 self
._execute
(lfsdef
, [], STATE_PREAMBLE
, STATE_PREAMBLE
)
1357 self
._execute
("\\normalsize%\n", [], STATE_PREAMBLE
, STATE_PREAMBLE
)
1358 self
._execute
("\\newdimen\\linewidth\\newdimen\\textwidth%\n", [], STATE_PREAMBLE
, STATE_PREAMBLE
)
1361 class SingleLatexRunner(SingleRunner
):
1363 #: default :class:`texmessage` parsers at LaTeX class loading
1364 texmessages_docclass_default
= [texmessage
.load
]
1365 #: default :class:`texmessage` parsers at ``\begin{document}``
1366 texmessages_begindoc_default
= [texmessage
.load
, texmessage
.no_aux
]
1368 def __init__(self
, cmd
=config
.getlist("text", "latex", ["latex"]),
1369 docclass
="article", docopt
=None, pyxgraphics
=True,
1370 texmessages_docclass
=[], texmessages_begindoc
=[], **kwargs
):
1373 This class adjusts the :class:`SingleRunner` to use LaTeX.
1375 :param cmd: command and arguments to start the TeX interpreter
1377 :type cmd: list of str
1378 :param str docclass: document class
1379 :param docopt: document loading options
1380 :type docopt: str or None
1381 :param bool pyxgraphics: activate graphics bundle support, see
1383 :param texmessages_docclass: additional message parsers at LaTeX class
1385 :type texmessages_docclass: list of :class:`texmessage` parsers
1386 :param texmessages_begindoc: additional message parsers at
1387 ``\\begin{document}``
1388 :type texmessages_begindoc: list of :class:`texmessage` parsers
1389 :param kwargs: additional arguments passed to :class:`SingleRunner`
1392 super().__init
__(cmd
=cmd
, **kwargs
)
1393 self
.docclass
= docclass
1394 self
.docopt
= docopt
1395 self
.pyxgraphics
= pyxgraphics
1396 self
.texmessages_docclass
= texmessages_docclass
1397 self
.texmessages_begindoc
= texmessages_begindoc
1400 def go_typeset(self
):
1401 self
._execute
("\\begin{document}", self
.texmessages_begindoc_default
+ self
.texmessages_begindoc
, STATE_PREAMBLE
, STATE_TYPESET
)
1403 def go_finish(self
):
1404 self
._execute
("\\end{document}%\n", self
.texmessages_end_default
+ self
.texmessages_end
, STATE_TYPESET
, STATE_DONE
)
1406 def force_done(self
):
1407 self
.texinput
.write("\n\\catcode`\\@11\\relax\\@@end\n")
1411 if self
.pyxgraphics
:
1412 with config
.open("pyx.def", []) as source
, open(os
.path
.join(self
.tmpdir
, "pyx.def"), "wb") as dest
:
1413 dest
.write(source
.read())
1414 self
._execute
("\\makeatletter%\n"
1415 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
1416 "\\def\\ProcessOptions{%\n"
1417 "\\def\\Gin@driver{" + self
.tmpdir
.replace(os
.sep
, "/") + "/pyx.def}%\n"
1418 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
1419 "\\saveProcessOptions}%\n"
1421 [], STATE_PREAMBLE
, STATE_PREAMBLE
)
1422 if self
.docopt
is not None:
1423 self
._execute
("\\documentclass[%s]{%s}" % (self
.docopt
, self
.docclass
),
1424 self
.texmessages_docclass_default
+ self
.texmessages_docclass
, STATE_PREAMBLE
, STATE_PREAMBLE
)
1426 self
._execute
("\\documentclass{%s}" % self
.docclass
,
1427 self
.texmessages_docclass_default
+ self
.texmessages_docclass
, STATE_PREAMBLE
, STATE_PREAMBLE
)
1430 def reset_for_tex_done(f
):
1432 def wrapped(self
, *args
, **kwargs
):
1434 return f(self
, *args
, **kwargs
)
1435 except TexDoneError
:
1436 self
.reset(reinit
=True)
1437 return f(self
, *args
, **kwargs
)
1443 def __init__(self
, cls
, *args
, **kwargs
):
1444 """A restartable :class:`SingleRunner` class
1446 :param cls: the class being wrapped
1447 :type cls: :class:`SingleRunner` class
1448 :param list args: args at class instantiation
1449 :param dict kwargs: keyword args at at class instantiation
1454 self
.kwargs
= kwargs
1457 def preamble(self
, expr
, texmessages
=[]):
1458 "resembles :meth:`SingleRunner.preamble`"
1459 self
.preambles
.append((expr
, texmessages
))
1460 self
.instance
.preamble(expr
, texmessages
)
1463 def text_pt(self
, *args
, **kwargs
):
1464 "resembles :meth:`SingleRunner.text_pt`"
1465 return self
.instance
.text_pt(*args
, **kwargs
)
1468 def text(self
, *args
, **kwargs
):
1469 "resembles :meth:`SingleRunner.text`"
1470 return self
.instance
.text(*args
, **kwargs
)
1472 def reset(self
, reinit
=False):
1473 """Start a new :class:`SingleRunner` instance
1475 :param bool reinit: replay :meth:`preamble` calls on the new instance
1477 After executing this function further preamble calls are allowed,
1478 whereas once a text output has been created, :meth:`preamble` calls are
1482 self
.instance
= self
.cls(*self
.args
, **self
.kwargs
)
1484 for expr
, texmessages
in self
.preambles
:
1485 self
.instance
.preamble(expr
, texmessages
)
1490 class TexRunner(MultiRunner
):
1492 def __init__(self
, *args
, **kwargs
):
1493 """A restartable :class:`SingleTexRunner` class
1495 :param list args: args at class instantiation
1496 :param dict kwargs: keyword args at at class instantiation
1499 super().__init
__(SingleTexRunner
, *args
, **kwargs
)
1502 class LatexRunner(MultiRunner
):
1504 def __init__(self
, *args
, **kwargs
):
1505 """A restartable :class:`SingleLatexRunner` class
1507 :param list args: args at class instantiation
1508 :param dict kwargs: keyword args at at class instantiation
1511 super().__init
__(SingleLatexRunner
, *args
, **kwargs
)
1514 # old, deprecated names:
1515 texrunner
= TexRunner
1516 latexrunner
= LatexRunner
1518 # module level interface documentation for autodoc
1519 # the actual values are setup by the set function
1521 #: the current :class:`MultiRunner` instance for the module level functions
1522 default_runner
= None
1524 #: default_runner.preamble (bound method)
1527 #: default_runner.text_pt (bound method)
1530 #: default_runner.text (bound method)
1533 #: default_runner.reset (bound method)
1536 def set(cls
=TexRunner
, mode
=None, *args
, **kwargs
):
1537 """Setup a new module level :class:`MultiRunner`
1539 :param cls: the module level :class:`MultiRunner` to be used, i.e.
1540 :class:`TexRunner` or :class:`LatexRunner`
1541 :type cls: :class:`MultiRunner` object, not instance
1542 :param mode: ``"tex"`` for :class:`TexRunner` or ``"latex"`` for
1543 :class:`LatexRunner` with arbitraty capitalization, overwriting the cls
1546 :deprecated: use the cls argument instead
1547 :type mode: str or None
1548 :param list args: args at class instantiation
1549 :param dict kwargs: keyword args at at class instantiation
1552 # note: defaulttexrunner is deprecated
1553 global default_runner
, defaulttexrunner
, reset
, preamble
, text
, text_pt
1554 if mode
is not None:
1555 logger
.warning("mode setting is deprecated, use the cls argument instead")
1556 cls
= {"tex": TexRunner
, "latex": LatexRunner
}[mode
.lower()]
1557 default_runner
= defaulttexrunner
= cls(*args
, **kwargs
)
1558 preamble
= default_runner
.preamble
1559 text_pt
= default_runner
.text_pt
1560 text
= default_runner
.text
1561 reset
= default_runner
.reset
1563 # initialize default_runner
1567 def escapestring(s
, replace
={" ": "~",
1579 "\\": "{$\setminus$}",
1581 "Escapes ASCII characters such that they can be typeset by TeX/LaTeX"""
1584 if not 32 <= ord(s[i]) < 127:
1585 raise ValueError("escapestring function handles ascii strings only
")
1592 s = s[:i] + r + s[i+1:]