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
, canvas
, 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): pass
186 start_pattern
= re
.compile(r
"This is [-0-9a-zA-Z\s_]*TeX")
189 def start(msg
, page
):
190 r
"""Message parser to check for proper TeX startup.
193 >>> texmessage.start(r'''
194 ... This is e-TeX (version)
195 ... *! Undefined control sequence.
202 # check for "This is e-TeX" etc.
203 if not texmessage
.start_pattern
.search(msg
):
204 raise TexResultError("TeX startup failed")
206 # check for \raiseerror -- just to be sure that communication works
207 new
= msg
.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[-1]
209 raise TexResultError("TeX scrollmode check failed")
213 def no_file(fileending
):
214 "Message parser generator for missing file with given fileending."
215 def check(msg
, page
):
216 "Message parser for missing {} file."
217 return msg
.replace("No file texput.%s." % fileending
, "").replace("No file %s%stexput.%s." % (os
.curdir
, os
.sep
, fileending
), "")
218 check
.__doc
__ = check
.__doc
__.format(fileending
)
221 no_aux
= no_file
.__func
__("aux")
222 no_nav
= no_file
.__func
__("nav")
224 aux_pattern
= re
.compile(r
'\(([^()]+\.aux|"[^"]+\.aux")\)')
225 dvi_pattern
= re
.compile(r
"Output written on .*texput\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\.", re
.DOTALL
)
226 log_pattern
= re
.compile(r
"Transcript written on .*texput\.log\.", re
.DOTALL
)
230 "Message parser to check for proper TeX shutdown."
231 msg
= re
.sub(texmessage
.aux_pattern
, "", msg
).replace("(see the transcript file for additional information)", "")
233 # check for "Output written on ...dvi (1 page, 220 bytes)."
235 msg
, m
= remove_pattern(texmessage
.dvi_pattern
, msg
)
237 raise TexResultError("TeX dvifile messages expected")
238 if m
.group("page") != str(page
):
239 raise TexResultError("wrong number of pages reported")
241 msg
, m
= remove_string("No pages of output.", msg
)
243 raise TexResultError("no dvifile expected")
245 # check for "Transcript written on ...log."
246 msg
, m
= remove_pattern(texmessage
.log_pattern
, msg
)
248 raise TexResultError("TeX logfile message expected")
251 quoted_file_pattern
= re
.compile(r
'\("(?P<filename>[^"]+)".*?\)')
252 file_pattern
= re
.compile(r
'\((?P<filename>[^"][^ )]*).*?\)', re
.DOTALL
)
256 """Message parser for loading of files.
258 Removes text starting with a round bracket followed by a filename
259 ignoring all further text until the corresponding closing bracket.
260 Quotes and/or line breaks in the filename are handled as needed to read
263 Without quoting the filename, the necessary removal of line breaks is
264 not well defined and the different possibilities are tested to check
265 whether one solution is ok. The last of the examples below checks this
269 >>> texmessage.load(r'''other (text.py) things''', 0)
271 >>> texmessage.load(r'''other ("text.py") things''', 0)
273 >>> texmessage.load(r'''other ("tex
274 ... t.py" further (ignored)
275 ... text) things''', 0)
277 >>> texmessage.load(r'''other (t
281 ... ther (ignored) text) things''', 0)
285 r
= remove_nested_brackets(msg
)
286 r
, m
= remove_pattern(texmessage
.quoted_file_pattern
, r
)
288 if not os
.path
.isfile(m
.group("filename")):
290 r
, m
= remove_pattern(texmessage
.quoted_file_pattern
, r
)
291 r
, m
= remove_pattern(texmessage
.file_pattern
, r
, ignore_nl
=False)
293 for filename
in itertools
.accumulate(m
.group("filename").split("\n")):
294 if os
.path
.isfile(filename
):
298 r
, m
= remove_pattern(texmessage
.file_pattern
, r
, ignore_nl
=False)
301 quoted_def_pattern
= re
.compile(r
'\("(?P<filename>[^"]+\.(fd|def))"\)')
302 def_pattern
= re
.compile(r
'\((?P<filename>[^"][^ )]*\.(fd|def))\)')
305 def load_def(msg
, page
):
306 "Message parser for loading of font definition files."
308 for p
in [texmessage
.quoted_def_pattern
, texmessage
.def_pattern
]:
309 r
, m
= remove_pattern(p
, r
)
311 if not os
.path
.isfile(m
.group("filename")):
313 r
, m
= remove_pattern(texmessage
.quoted_file_pattern
, r
)
316 quoted_graphics_pattern
= re
.compile(r
'<"(?P<filename>[^"]+\.eps)">')
317 graphics_pattern
= re
.compile(r
'<(?P<filename>[^"][^>]*\.eps)>')
320 def load_graphics(msg
, page
):
321 "Message parser for loading of graphics files."
323 for p
in [texmessage
.quoted_graphics_pattern
, texmessage
.graphics_pattern
]:
324 r
, m
= remove_pattern(p
, r
)
326 if not os
.path
.isfile(m
.group("filename")):
328 r
, m
= remove_pattern(texmessage
.quoted_file_pattern
, r
)
332 def ignore(msg
, page
):
333 """Message parser to ignore all output.
335 Should be used as a last resort only. You should write a proper message
336 parser checking for the output you observe.
343 """Message parser to warn about all output.
345 Similar to the :meth:`ignore` message parser, but writing a warning to
346 the logger about the message. This is considered to be better when you
347 need to get it working quickly as you will still be prompted about the
348 unresolved message, while the processing continues.
352 logger
.warning("ignoring TeX warnings:\n%s" % textwrap
.indent(msg
.rstrip(), " "))
356 def pattern(p
, warning
):
357 "Message parser generator for pattern matching."
358 def check(msg
, page
):
359 "Message parser for {}."
360 msg
, m
= remove_pattern(p
, msg
, ignore_nl
=False)
362 logger
.warning("ignoring %s:\n%s" % (warning
, m
.string
[m
.start(): m
.end()].rstrip()))
363 msg
, m
= remove_pattern(p
, msg
, ignore_nl
=False)
365 check
.__doc
__ = check
.__doc
__.format(warning
)
368 box_warning
= pattern
.__func
__(re
.compile(r
"^(Overfull|Underfull) \\[hv]box.*$(\n^..*$)*\n^$\n", re
.MULTILINE
), "overfull/underfull box warning")
369 font_warning
= pattern
.__func
__(re
.compile(r
"^LaTeX Font Warning: .*$(\n^\(Font\).*$)*", re
.MULTILINE
), "font warning")
370 package_warning
= pattern
.__func
__(re
.compile(r
"^package\s+(?P<packagename>\S+)\s+warning\s*:[^\n]+(?:\n\(?(?P=packagename)\)?[^\n]*)*", re
.MULTILINE | re
.IGNORECASE
), "generic package warning")
371 rerun_warning
= pattern
.__func
__(re
.compile(r
"^(LaTeX Warning: Label\(s\) may have changed\. Rerun to get cross-references right\s*\.)$", re
.MULTILINE
), "rerun warning")
372 nobbl_warning
= pattern
.__func
__(re
.compile(r
"^[\s\*]*(No file .*\.bbl.)\s*", re
.MULTILINE
), "no-bbl warning")
376 ###############################################################################
378 ###############################################################################
380 _textattrspreamble
= ""
383 "a textattr defines a apply method, which modifies a (La)TeX expression"
385 class _localattr
: pass
387 _textattrspreamble
+= r
"""\gdef\PyXFlushHAlign{0}%
389 \leftskip=0pt plus \PyXFlushHAlign fil%
390 \rightskip=0pt plus 1fil%
391 \advance\rightskip0pt plus -\PyXFlushHAlign fil%
397 \exhyphenpenalty=9999}%
400 class boxhalign(attr
.exclusiveattr
, textattr
, _localattr
):
402 def __init__(self
, aboxhalign
):
403 self
.boxhalign
= aboxhalign
404 attr
.exclusiveattr
.__init
__(self
, boxhalign
)
406 def apply(self
, expr
):
407 return r
"\gdef\PyXBoxHAlign{%.5f}%s" % (self
.boxhalign
, expr
)
409 boxhalign
.left
= boxhalign(0)
410 boxhalign
.center
= boxhalign(0.5)
411 boxhalign
.right
= boxhalign(1)
412 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass for boxhalign since it can't clear a halign's boxhalign
415 class flushhalign(attr
.exclusiveattr
, textattr
, _localattr
):
417 def __init__(self
, aflushhalign
):
418 self
.flushhalign
= aflushhalign
419 attr
.exclusiveattr
.__init
__(self
, flushhalign
)
421 def apply(self
, expr
):
422 return r
"\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self
.flushhalign
, expr
)
424 flushhalign
.left
= flushhalign(0)
425 flushhalign
.center
= flushhalign(0.5)
426 flushhalign
.right
= flushhalign(1)
427 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
430 class halign(boxhalign
, flushhalign
, _localattr
):
432 def __init__(self
, aboxhalign
, aflushhalign
):
433 self
.boxhalign
= aboxhalign
434 self
.flushhalign
= aflushhalign
435 attr
.exclusiveattr
.__init
__(self
, halign
)
437 def apply(self
, expr
):
438 return r
"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self
.boxhalign
, self
.flushhalign
, expr
)
440 halign
.left
= halign(0, 0)
441 halign
.center
= halign(0.5, 0.5)
442 halign
.right
= halign(1, 1)
443 halign
.clear
= attr
.clearclass(halign
)
444 halign
.boxleft
= boxhalign
.left
445 halign
.boxcenter
= boxhalign
.center
446 halign
.boxright
= boxhalign
.right
447 halign
.flushleft
= halign
.raggedright
= flushhalign
.left
448 halign
.flushcenter
= halign
.raggedcenter
= flushhalign
.center
449 halign
.flushright
= halign
.raggedleft
= flushhalign
.right
452 class _mathmode(attr
.attr
, textattr
, _localattr
):
455 def apply(self
, expr
):
456 return r
"$\displaystyle{%s}$" % expr
458 mathmode
= _mathmode()
459 clearmathmode
= attr
.clearclass(_mathmode
)
462 class _phantom(attr
.attr
, textattr
, _localattr
):
465 def apply(self
, expr
):
466 return r
"\phantom{%s}" % expr
469 clearphantom
= attr
.clearclass(_phantom
)
472 _textattrspreamble
+= "\\newbox\\PyXBoxVBox%\n\\newdimen\\PyXDimenVBox%\n"
474 class parbox_pt(attr
.sortbeforeexclusiveattr
, textattr
):
480 def __init__(self
, width
, baseline
=top
):
481 self
.width
= width
* 72.27 / (unit
.scale
["x"] * 72)
482 self
.baseline
= baseline
483 attr
.sortbeforeexclusiveattr
.__init
__(self
, parbox_pt
, [_localattr
])
485 def apply(self
, expr
):
486 if self
.baseline
== self
.top
:
487 return r
"\linewidth=%.5ftruept\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self
.width
, expr
)
488 elif self
.baseline
== self
.middle
:
489 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
)
490 elif self
.baseline
== self
.bottom
:
491 return r
"\linewidth=%.5ftruept\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self
.width
, expr
)
493 ValueError("invalid baseline argument")
495 parbox_pt
.clear
= attr
.clearclass(parbox_pt
)
497 class parbox(parbox_pt
):
499 def __init__(self
, width
, **kwargs
):
500 parbox_pt
.__init
__(self
, unit
.topt(width
), **kwargs
)
502 parbox
.clear
= parbox_pt
.clear
505 _textattrspreamble
+= "\\newbox\\PyXBoxVAlign%\n\\newdimen\\PyXDimenVAlign%\n"
507 class valign(attr
.sortbeforeexclusiveattr
, textattr
):
509 def __init__(self
, avalign
):
510 self
.valign
= avalign
511 attr
.sortbeforeexclusiveattr
.__init
__(self
, valign
, [parbox_pt
, _localattr
])
513 def apply(self
, expr
):
514 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
)
516 valign
.top
= valign(0)
517 valign
.middle
= valign(0.5)
518 valign
.bottom
= valign(1)
519 valign
.clear
= valign
.baseline
= attr
.clearclass(valign
)
522 _textattrspreamble
+= "\\newdimen\\PyXDimenVShift%\n"
524 class _vshift(attr
.sortbeforeattr
, textattr
):
527 attr
.sortbeforeattr
.__init
__(self
, [valign
, parbox_pt
, _localattr
])
529 def apply(self
, expr
):
530 return r
"%s\setbox0\hbox{{%s}}\lower\PyXDimenVShift\box0" % (self
.setheightexpr(), expr
)
532 class vshift(_vshift
):
533 "vertical down shift by a fraction of a character height"
535 def __init__(self
, lowerratio
, heightstr
="0"):
536 _vshift
.__init
__(self
)
537 self
.lowerratio
= lowerratio
538 self
.heightstr
= heightstr
540 def setheightexpr(self
):
541 return r
"\setbox0\hbox{{%s}}\PyXDimenVShift=%.5f\ht0" % (self
.heightstr
, self
.lowerratio
)
543 class _vshiftmathaxis(_vshift
):
544 "vertical down shift by the height of the math axis"
546 def setheightexpr(self
):
547 return r
"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\PyXDimenVShift=\ht0"
550 vshift
.bottomzero
= vshift(0)
551 vshift
.middlezero
= vshift(0.5)
552 vshift
.topzero
= vshift(1)
553 vshift
.mathaxis
= _vshiftmathaxis()
554 vshift
.clear
= attr
.clearclass(_vshift
)
557 defaultsizelist
= ["normalsize", "large", "Large", "LARGE", "huge", "Huge",
558 None, "tiny", "scriptsize", "footnotesize", "small"]
560 class size(attr
.sortbeforeattr
, textattr
):
563 def __init__(self
, sizeindex
=None, sizename
=None, sizelist
=defaultsizelist
):
564 if (sizeindex
is None and sizename
is None) or (sizeindex
is not None and sizename
is not None):
565 raise ValueError("either specify sizeindex or sizename")
566 attr
.sortbeforeattr
.__init
__(self
, [_mathmode
, _vshift
])
567 if sizeindex
is not None:
568 if sizeindex
>= 0 and sizeindex
< sizelist
.index(None):
569 self
.size
= sizelist
[sizeindex
]
570 elif sizeindex
< 0 and sizeindex
+ len(sizelist
) > sizelist
.index(None):
571 self
.size
= sizelist
[sizeindex
]
573 raise IndexError("index out of sizelist range")
577 def apply(self
, expr
):
578 return r
"\%s{}%s" % (self
.size
, expr
)
581 size
.scriptsize
= size
.script
= size(-3)
582 size
.footnotesize
= size
.footnote
= size(-2)
583 size
.small
= size(-1)
584 size
.normalsize
= size
.normal
= size(0)
590 size
.clear
= attr
.clearclass(size
)
593 ###############################################################################
595 ###############################################################################
598 class MonitorOutput(threading
.Thread
):
600 def __init__(self
, name
, output
):
601 """Deadlock-safe output stream reader and monitor.
603 This method sets up a thread to continously read lines from a stream.
604 By that a deadlock due to a full pipe is prevented. In addition, the
605 stream content can be monitored for containing a certain string (see
606 :meth:`expect` and :meth:`wait`) and return all the collected output
609 :param string name: name to be used while logging in :meth:`wait` and
611 :param file output: output stream
615 self
._expect
= queue
.Queue(1)
616 self
._received
= threading
.Event()
617 self
._output
= queue
.Queue()
618 threading
.Thread
.__init
__(self
, name
=name
, daemon
=1)
622 """Expect a string on a **single** line in the output.
624 This method must be called **before** the output occurs, i.e. before
625 the input is written to the TeX/LaTeX process.
627 :param s: expected string or ``None`` if output is expected to become
632 self
._expect
.put_nowait(s
)
635 """Read all output collected since its previous call.
637 The output reading should be synchronized by the :meth:`expect`
638 and :meth:`wait` methods.
640 :returns: collected output from the stream
647 l
.append(self
._output
.get_nowait())
650 return "".join(l
).replace("\r\n", "\n").replace("\r", "\n")
652 def _wait(self
, waiter
, checker
):
653 """Helper method to implement :meth:`wait` and :meth:`done`.
655 Waits for an event using the *waiter* and *checker* functions while
656 providing user feedback to the ``pyx``-logger using the warning level
657 according to the ``wait`` and ``showwait`` from the ``text`` section of
658 the pyx :mod:`config`.
660 :param function waiter: callback to wait for (the function gets called
661 with a timeout parameter)
662 :param function checker: callback returing ``True`` if
663 waiting was successful
664 :returns: ``True`` when wait was successful
668 wait
= config
.getint("text", "wait", 60)
669 showwait
= config
.getint("text", "showwait", 5)
673 while waited
< wait
and not hasevent
:
674 if wait
- waited
> showwait
:
678 waiter(wait
- waited
)
679 waited
+= wait
- waited
683 logger
.warning("Still waiting for {} "
684 "after {} (of {}) seconds..."
685 .format(self
.name
, waited
, wait
))
687 logger
.warning("The timeout of {} seconds expired "
688 "and {} did not respond."
689 .format(waited
, self
.name
))
696 """Wait for the expected output to happen.
698 Waits either until a line containing the string set by the previous
699 :meth:`expect` call is found, or a timeout occurs.
701 :returns: ``True`` when the expected string was found
705 r
= self
._wait
(self
._received
.wait
, self
._received
.isSet
)
707 self
._received
.clear()
711 """Waits until the output becomes empty.
713 Waits either until the output becomes empty, or a timeout occurs.
714 The generated output can still be catched by :meth:`read` after
715 :meth:`done` was successful.
717 In the proper workflow :meth:`expect` should be called with ``None``
718 before the output completes, as otherwise a ``ValueError`` is raised
721 :returns: ``True`` when the output has become empty
725 return self
._wait
(self
.join
, lambda self
=self
: not self
.is_alive())
728 """Read a line from the output.
730 To be used **inside** the thread routine only.
732 :returns: one line of the output as a string
738 return self
.output
.readline()
740 if e
.errno
!= errno
.EINTR
:
746 **Not** to be called from outside.
748 :raises ValueError: output becomes empty while some string is expected
753 line
= self
._readline
()
756 expect
= self
._expect
.get_nowait()
761 self
._output
.put(line
)
762 if expect
is not None:
763 found
= line
.find(expect
)
768 if expect
is not None:
769 raise ValueError("{} finished unexpectedly".format(self
.name
))
772 class textbox(box
.rect
, canvas
.canvas
):
773 """basically a box.rect, but it contains a text created by the texrunner
774 - texrunner._text and texrunner.text return such an object
775 - _textbox instances can be inserted into a canvas
776 - the output is contained in a page of the dvifile available thru the texrunner"""
777 # TODO: shouldn't all boxes become canvases? how about inserts then?
779 def __init__(self
, x
, y
, left
, right
, height
, depth
, do_finish
, attrs
):
781 - do_finish is a method to be called to get the dvicanvas
782 (e.g. the do_finish calls the setdvicanvas method)
783 - attrs are fillstyles"""
786 self
.width
= left
+ right
789 self
.texttrafo
= trafo
.scale(unit
.scale
["x"]).translated(x
, y
)
790 box
.rect
.__init
__(self
, x
- left
, y
- depth
, left
+ right
, depth
+ height
, abscenter
= (left
, depth
))
791 canvas
.canvas
.__init
__(self
, attrs
)
792 self
.do_finish
= do_finish
793 self
.dvicanvas
= None
794 self
.insertdvicanvas
= False
796 def transform(self
, *trafos
):
797 if self
.insertdvicanvas
:
798 raise ValueError("can't apply transformation after dvicanvas was inserted")
799 box
.rect
.transform(self
, *trafos
)
801 self
.texttrafo
= trafo
* self
.texttrafo
803 def setdvicanvas(self
, dvicanvas
):
804 if self
.dvicanvas
is not None:
805 raise ValueError("multiple call to setdvicanvas")
806 self
.dvicanvas
= dvicanvas
808 def ensuredvicanvas(self
):
809 if self
.dvicanvas
is None:
811 assert self
.dvicanvas
is not None, "do_finish is broken"
812 if not self
.insertdvicanvas
:
813 self
.insert(self
.dvicanvas
, [self
.texttrafo
])
814 self
.insertdvicanvas
= True
816 def marker(self
, marker
):
817 self
.ensuredvicanvas()
818 return self
.texttrafo
.apply(*self
.dvicanvas
.markers
[marker
])
821 self
.ensuredvicanvas()
822 textpath
= path
.path()
823 for item
in self
.dvicanvas
.items
:
825 textpath
+= item
.textpath()
826 except AttributeError:
827 # ignore color settings etc.
829 return textpath
.transformed(self
.texttrafo
)
831 def processPS(self
, file, writer
, context
, registry
, bbox
):
832 self
.ensuredvicanvas()
833 abbox
= bboxmodule
.empty()
834 canvas
.canvas
.processPS(self
, file, writer
, context
, registry
, abbox
)
835 bbox
+= box
.rect
.bbox(self
)
837 def processPDF(self
, file, writer
, context
, registry
, bbox
):
838 self
.ensuredvicanvas()
839 abbox
= bboxmodule
.empty()
840 canvas
.canvas
.processPDF(self
, file, writer
, context
, registry
, abbox
)
841 bbox
+= box
.rect
.bbox(self
)
850 def __init__(self
, *files
):
853 def write(self
, data
):
854 for file in self
.files
:
858 for file in self
.files
:
862 for file in self
.files
:
865 # The texrunner state represents the next (or current) execute state.
866 STATE_START
, STATE_PREAMBLE
, STATE_TYPESET
, STATE_DONE
= range(4)
867 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:")
871 defaulttexmessagesstart
= [texmessage
.start
]
872 defaulttexmessagesend
= [texmessage
.end
, texmessage
.font_warning
, texmessage
.rerun_warning
, texmessage
.nobbl_warning
]
873 defaulttexmessagesdefaultpreamble
= [texmessage
.load
]
874 defaulttexmessagesdefaultrun
= [texmessage
.font_warning
, texmessage
.box_warning
, texmessage
.package_warning
,
875 texmessage
.load_def
, texmessage
.load_graphics
]
877 def __init__(self
, executable
,
880 texipc
=config
.getboolean("text", "texipc", 0),
888 """Base class for the TeX interface.
890 .. note:: This class cannot be used directly. It is the base class for
891 all texrunners and provides most of the implementation.
892 Still, to the end user the parameters except for *executable*
893 are important, as they are preserved in derived classes
896 :param str executable: command to start the TeX interpreter
897 :param str texenc: encoding to use in the communication with the TeX
899 :param usefiles: list of supplementary files
900 :type usefiles: list of str
901 :param bool texipc: :ref:`texipc` flag. The default value shown is an
902 artifact of the documentation build environment. The actual default
903 is defined by the *texipc* option of the *text* section of the
905 :param texdebug: filename or file to be used to store a copy of all the
906 input passed to the TeX interpreter
907 :type texdebug: None or str or file
908 :param bool dvidebug: ``True`` to turn on dvitype-like output
909 :param int errordebug: verbosity of the TexResultError messages
910 (``0`` means without the input and output details,
911 ``1`` means input and parsed output shortend to 5 lines, and
912 ``2`` means full input and unparsed as well as parsed output)
913 :param texmsgstart: additional message parsers to parse the TeX
915 :type texmsgstart: list of texmsg
916 :param texmsgend: additional message parsers to parse the TeX
918 :type texmsgend: list of texmsg
919 :param texmsgpreamble: additional message parsers to parse the preamble
921 :type texmsgpreamble: list of texmsg
922 :param texmsgrun: additional message parsers to parse the typeset
924 :type texmsgrun: list of texmsg
926 self
.executable
= executable
928 self
.usefiles
= usefiles
930 self
.texdebug
= texdebug
931 self
.dvidebug
= dvidebug
932 self
.errordebug
= errordebug
933 self
.texmsgstart
= texmsgstart
934 self
.texmsgend
= texmsgend
935 self
.texmsgpreamble
= texmsgpreamble
936 self
.texmsgrun
= texmsgrun
938 self
.state
= STATE_START
943 self
.needdvitextboxes
= [] # when texipc-mode off
945 self
.textboxesincluded
= 0
950 """Clean-up TeX interpreter and tmp directory.
952 This funtion is hooked up in atexit to quit the TeX interpreter, to
953 save contents of the usefile files, and to remove the temporary
958 if self
.state
> STATE_START
:
959 if self
.state
< STATE_DONE
:
961 if self
.state
< STATE_DONE
: # cleanup while TeX is still running?
962 self
.texoutput
.expect(None)
964 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
)),
965 (self
.popen
.terminate
, "Failed, too. Trying by kill signal now ..."),
966 (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
))]:
968 if self
.texoutput
.done():
971 for usefile
in self
.usefiles
:
972 extpos
= usefile
.rfind(".")
974 os
.rename(os
.path
.join(self
.tmpdir
, "texput" + usefile
[extpos
:]), usefile
)
975 except EnvironmentError:
976 logger
.warning("Could not save '{}'.".format(usefile
))
977 if os
.path
.isfile(usefile
):
980 except EnvironmentError:
981 logger
.warning("Failed to remove spurious file '{}'.".format(usefile
))
983 shutil
.rmtree(self
.tmpdir
, ignore_errors
=True)
985 def _execute(self
, expr
, texmessages
, oldstate
, newstate
):
986 """executes expr within TeX/LaTeX"""
987 assert STATE_PREAMBLE
<= oldstate
<= STATE_TYPESET
988 assert oldstate
== self
.state
989 assert newstate
>= oldstate
990 if newstate
== STATE_DONE
:
991 self
.texoutput
.expect(None)
992 self
.texinput
.write(expr
)
994 if oldstate
== newstate
== STATE_TYPESET
:
996 expr
= "\\ProcessPyXBox{%s%%\n}{%i}" % (expr
, self
.page
)
998 self
.texoutput
.expect("PyXInputMarker:executeid=%i:" % self
.executeid
)
999 expr
+= "%%\n\\PyXInput{%i}%%\n" % self
.executeid
1000 self
.texinput
.write(expr
)
1001 self
.texinput
.flush()
1002 self
.state
= newstate
1003 if newstate
== STATE_DONE
:
1004 wait_ok
= self
.texoutput
.done()
1006 wait_ok
= self
.texoutput
.wait()
1008 parsed
= unparsed
= self
.texoutput
.read()
1010 raise TexResultError("TeX didn't respond as expected within the timeout period.")
1011 if newstate
!= STATE_DONE
:
1012 parsed
, m
= remove_string("PyXInputMarker:executeid=%s:" % self
.executeid
, parsed
)
1014 raise TexResultError("PyXInputMarker expected")
1015 if oldstate
== newstate
== STATE_TYPESET
:
1016 parsed
, m
= remove_pattern(PyXBoxPattern
, parsed
, ignore_nl
=False)
1018 raise TexResultError("PyXBox expected")
1019 if m
.group("page") != str(self
.page
):
1020 raise TexResultError("Wrong page number in PyXBox")
1021 extent
= [float(x
)*72/72.27*unit
.x_pt
for x
in m
.group("lt", "rt", "ht", "dp")]
1022 parsed
, m
= remove_string("[80.121.88.%s]" % self
.page
, parsed
)
1024 raise TexResultError("PyXPageOutMarker expected")
1025 for t
in texmessages
:
1026 parsed
= t(parsed
, self
.page
)
1027 if parsed
.replace(r
"(Please type a command or say `\end')", "").replace(" ", "").replace("*\n", "").replace("\n", ""):
1028 raise TexResultError("unhandled TeX response (might be an error)")
1029 except TexResultError
as e
:
1030 if self
.errordebug
> 0:
1031 def add(msg
): e
.args
= (e
.args
[0] + msg
,)
1032 add("\nThe expression passed to TeX was:\n{}".format(textwrap
.indent(expr
.rstrip(), " ")))
1033 if self
.errordebug
>= 2:
1034 add("\nThe return message from TeX was:\n{}".format(textwrap
.indent(unparsed
.rstrip(), " ")))
1035 if self
.errordebug
== 1:
1036 if parsed
.count('\n') > 5:
1037 parsed
= "\n".join(parsed
.split("\n")[:5] + ["(cut after 5 lines; use errordebug=2 for all output)"])
1038 add("\nAfter parsing the return message from TeX, the following was left:\n{}".format(textwrap
.indent(parsed
.rstrip(), " ")))
1040 if oldstate
== newstate
== STATE_TYPESET
:
1044 assert self
.state
== STATE_START
1045 self
.state
= STATE_PREAMBLE
1047 if self
.tmpdir
is None:
1048 self
.tmpdir
= tempfile
.mkdtemp()
1049 atexit
.register(self
._cleanup
)
1050 for usefile
in self
.usefiles
:
1051 extpos
= usefile
.rfind(".")
1053 os
.rename(usefile
, os
.path
.join(self
.tmpdir
, "texput" + usefile
[extpos
:]))
1056 cmd
= [self
.executable
, '--output-directory', self
.tmpdir
]
1059 self
.popen
= config
.Popen(cmd
, stdin
=config
.PIPE
, stdout
=config
.PIPE
, stderr
=config
.STDOUT
, bufsize
=0)
1060 self
.texinput
= io
.TextIOWrapper(self
.popen
.stdin
, encoding
=self
.texenc
)
1064 except AttributeError:
1065 self
.texinput
= Tee(open(self
.texdebug
, "w", encoding
=self
.texenc
), self
.texinput
)
1067 self
.texinput
= Tee(self
.texdebug
, self
.texinput
)
1068 self
.texoutput
= MonitorOutput(self
.name
, io
.TextIOWrapper(self
.popen
.stdout
, encoding
=self
.texenc
))
1069 self
._execute
("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
1070 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
1071 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
1072 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
1073 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
1074 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
1075 "\\newdimen\\PyXDimenHAlignRT%\n" +
1076 _textattrspreamble
+ # insert preambles for textattrs macros
1077 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
1078 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
1079 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
1080 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
1081 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
1082 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
1083 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
1084 "lt=\\the\\PyXDimenHAlignLT,"
1085 "rt=\\the\\PyXDimenHAlignRT,"
1086 "ht=\\the\\ht\\PyXBox,"
1087 "dp=\\the\\dp\\PyXBox:}%\n"
1088 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
1089 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
1090 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
1091 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
1092 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%", # write PyXMarker special into the dvi-file
1093 self
.defaulttexmessagesstart
+ self
.texmsgstart
, STATE_PREAMBLE
, STATE_PREAMBLE
)
1095 def do_preamble(self
, expr
, texmessages
):
1096 if self
.state
< STATE_PREAMBLE
:
1098 self
._execute
(expr
, texmessages
, STATE_PREAMBLE
, STATE_PREAMBLE
)
1100 def do_typeset(self
, expr
, texmessages
):
1101 if self
.state
< STATE_PREAMBLE
:
1103 if self
.state
< STATE_TYPESET
:
1105 return self
._execute
(expr
, texmessages
, STATE_TYPESET
, STATE_TYPESET
)
1107 def do_finish(self
):
1108 if self
.state
< STATE_TYPESET
:
1111 self
.texinput
.close() # close the input queue and
1112 self
.texoutput
.done() # wait for finish of the output
1115 dvifilename
= os
.path
.join(self
.tmpdir
, "texput.dvi")
1116 self
.dvifile
= dvifile
.DVIfile(dvifilename
, debug
=self
.dvidebug
)
1118 for box
in self
.needdvitextboxes
:
1119 box
.setdvicanvas(self
.dvifile
.readpage([ord("P"), ord("y"), ord("X"), page
, 0, 0, 0, 0, 0, 0], fontmap
=box
.fontmap
, singlecharmode
=box
.singlecharmode
))
1121 if self
.dvifile
.readpage(None) is not None:
1122 raise ValueError("end of dvifile expected but further pages follow")
1125 self
.needdvitextboxes
= []
1127 def reset(self
, reinit
=0):
1128 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
1129 assert self
.state
> STATE_START
1130 if self
.state
< STATE_DONE
:
1134 self
.state
= STATE_START
1136 for expr
, texmessages
in self
.preambles
:
1137 self
.do_preamble(expr
, texmessages
)
1141 def preamble(self
, expr
, texmessages
=[]):
1142 r
"""put something into the TeX/LaTeX preamble
1143 - in LaTeX, this is done before the \begin{document}
1144 (you might use \AtBeginDocument, when you're in need for)
1145 - it is not allowed to call preamble after calling the
1146 text method for the first time (for LaTeX this is needed
1147 due to \begin{document}; in TeX it is forced for compatibility
1148 (you should be able to switch from TeX to LaTeX, if you want,
1149 without breaking something)
1150 - preamble expressions must not create any dvi output
1151 - args might contain texmessage instances"""
1152 texmessages
= self
.defaulttexmessagesdefaultpreamble
+ self
.texmsgpreamble
+ texmessages
1153 self
.preambles
.append((expr
, texmessages
))
1154 self
.do_preamble(expr
, texmessages
)
1156 def text(self
, x
, y
, expr
, textattrs
=[], texmessages
=[], fontmap
=None, singlecharmode
=False):
1157 """create text by passing expr to TeX/LaTeX
1158 - returns a textbox containing the result from running expr thru TeX/LaTeX
1159 - the box center is set to x, y
1160 - *args may contain attr parameters, namely:
1161 - textattr instances
1162 - texmessage instances
1163 - trafo._trafo instances
1164 - style.fillstyle instances"""
1166 raise ValueError("None expression is invalid")
1167 if self
.state
== STATE_DONE
:
1168 self
.reset(reinit
=1)
1169 textattrs
= attr
.mergeattrs(textattrs
) # perform cleans
1170 attr
.checkattrs(textattrs
, [textattr
, trafo
.trafo_pt
, style
.fillstyle
])
1171 trafos
= attr
.getattrs(textattrs
, [trafo
.trafo_pt
])
1172 fillstyles
= attr
.getattrs(textattrs
, [style
.fillstyle
])
1173 textattrs
= attr
.getattrs(textattrs
, [textattr
])
1174 for ta
in textattrs
[::-1]:
1175 expr
= ta
.apply(expr
)
1176 first
= self
.state
< STATE_TYPESET
1177 left
, right
, height
, depth
= self
.do_typeset(expr
, self
.defaulttexmessagesdefaultrun
+ self
.texmsgrun
+ texmessages
)
1178 if self
.texipc
and first
:
1179 self
.dvifile
= dvifile
.DVIfile(os
.path
.join(self
.tmpdir
, "texput.dvi"), debug
=self
.dvidebug
)
1180 box
= textbox(x
, y
, left
, right
, height
, depth
, self
.do_finish
, fillstyles
)
1182 box
.reltransform(t
) # TODO: should trafos really use reltransform???
1183 # this is quite different from what we do elsewhere!!!
1184 # see https://sourceforge.net/mailarchive/forum.php?thread_id=9137692&forum_id=23700
1186 box
.setdvicanvas(self
.dvifile
.readpage([ord("P"), ord("y"), ord("X"), self
.page
, 0, 0, 0, 0, 0, 0], fontmap
=fontmap
, singlecharmode
=singlecharmode
))
1188 box
.fontmap
= fontmap
1189 box
.singlecharmode
= singlecharmode
1190 self
.needdvitextboxes
.append(box
)
1193 def text_pt(self
, x
, y
, expr
, *args
, **kwargs
):
1194 return self
.text(x
* unit
.t_pt
, y
* unit
.t_pt
, expr
, *args
, **kwargs
)
1197 class texrunner(_texrunner
):
1199 def __init__(self
, executable
=config
.get("text", "tex", "tex"), lfs
="10pt", **kwargs
):
1200 super().__init
__(executable
=executable
, **kwargs
)
1204 def go_typeset(self
):
1205 assert self
.state
== STATE_PREAMBLE
1206 self
.state
= STATE_TYPESET
1208 def go_finish(self
):
1209 self
._execute
("\\end%\n", self
.defaulttexmessagesend
+ self
.texmsgend
, STATE_TYPESET
, STATE_DONE
)
1211 def force_done(self
):
1212 self
.texinput
.write("\n\\end\n")
1217 if not self
.lfs
.endswith(".lfs"):
1218 self
.lfs
= "%s.lfs" % self
.lfs
1219 with config
.open(self
.lfs
, []) as lfsfile
:
1220 lfsdef
= lfsfile
.read().decode("ascii")
1221 self
._execute
(lfsdef
, [], STATE_PREAMBLE
, STATE_PREAMBLE
)
1222 self
._execute
("\\normalsize%\n", [], STATE_PREAMBLE
, STATE_PREAMBLE
)
1223 self
._execute
("\\newdimen\\linewidth\\newdimen\\textwidth%\n", [], STATE_PREAMBLE
, STATE_PREAMBLE
)
1226 class latexrunner(_texrunner
):
1228 defaulttexmessagesdocclass
= [texmessage
.load
]
1229 defaulttexmessagesbegindoc
= [texmessage
.load
, texmessage
.no_aux
]
1231 def __init__(self
, executable
=config
.get("text", "latex", "latex"),
1232 docclass
="article", docopt
=None, pyxgraphics
=True,
1233 texmessagesdocclass
=[], texmessagesbegindoc
=[], **kwargs
):
1234 super().__init
__(executable
=executable
, **kwargs
)
1235 self
.docclass
= docclass
1236 self
.docopt
= docopt
1237 self
.pyxgraphics
= pyxgraphics
1238 self
.texmessagesdocclass
= texmessagesdocclass
1239 self
.texmessagesbegindoc
= texmessagesbegindoc
1242 def go_typeset(self
):
1243 self
._execute
("\\begin{document}", self
.defaulttexmessagesbegindoc
+ self
.texmessagesbegindoc
, STATE_PREAMBLE
, STATE_TYPESET
)
1245 def go_finish(self
):
1246 self
._execute
("\\end{document}%\n", self
.defaulttexmessagesend
+ self
.texmsgend
, STATE_TYPESET
, STATE_DONE
)
1248 def force_done(self
):
1249 self
.texinput
.write("\n\\catcode`\\@11\\relax\\@@end\n")
1253 if self
.pyxgraphics
:
1254 with config
.open("pyx.def", []) as source
, open(os
.path
.join(self
.tmpdir
, "pyx.def"), "wb") as dest
:
1255 dest
.write(source
.read())
1256 self
._execute
("\\makeatletter%\n"
1257 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
1258 "\\def\\ProcessOptions{%\n"
1259 "\\def\\Gin@driver{" + self
.tmpdir
.replace(os
.sep
, "/") + "/pyx.def}%\n"
1260 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
1261 "\\saveProcessOptions}%\n"
1263 [], STATE_PREAMBLE
, STATE_PREAMBLE
)
1264 if self
.docopt
is not None:
1265 self
._execute
("\\documentclass[%s]{%s}" % (self
.docopt
, self
.docclass
),
1266 self
.defaulttexmessagesdocclass
+ self
.texmessagesdocclass
, STATE_PREAMBLE
, STATE_PREAMBLE
)
1268 self
._execute
("\\documentclass{%s}" % self
.docclass
,
1269 self
.defaulttexmessagesdocclass
+ self
.texmessagesdocclass
, STATE_PREAMBLE
, STATE_PREAMBLE
)
1272 def set(mode
="tex", **kwargs
):
1273 global defaulttexrunner
, reset
, preamble
, text
, text_pt
1276 defaulttexrunner
= texrunner(**kwargs
)
1277 elif mode
== "latex":
1278 defaulttexrunner
= latexrunner(**kwargs
)
1280 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1281 reset
= defaulttexrunner
.reset
1282 preamble
= defaulttexrunner
.preamble
1283 text
= defaulttexrunner
.text
1284 text_pt
= defaulttexrunner
.text_pt
1288 def escapestring(s
, replace
={" ": "~",
1300 "\\": "{$\setminus$}",
1302 "escape all ascii characters such that they are printable by TeX/LaTeX"
1305 if not 32 <= ord(s
[i
]) < 127:
1306 raise ValueError("escapestring function handles ascii strings only")
1313 s
= s
[:i
] + r
+ s
[i
+1:]