% \section{Included Python scripts} % \label{sec:included-scripts} % % Here we describe the Python code for \ST scripts, for running % Sage only if necessary, substituting in Sage outputs to produce % a ``static'' file, and extracting all Sage code from a |.tex| file. % % \subsection{\texttt{sagetex-run}} % \label{sec:sagetex-run} % \iffalse %<*ifnecessaryscript> % \fi % % When working on a document that uses \ST, running Sage every time you % typeset your document may take too long, especially since it often % is not necessary. This script is a drop-in replacement for Sage: % instead of % \begin{center} % |sage document.sagetex.sage| % \end{center} % you can do % \begin{center} % |sagetex-run document.sagetex.sage| % \end{center} % and it will use the MD5 mechanism described in the |endofdoc| macro % (page~{\pageref{macro:endofdoc}). With this, you can set up your editor % (\TeX Shop, \TeX Works, etc) to typeset your document with a script % that does % \begin{quote} % |pdflatex $1|\\ % |sagetex-run $1| % \end{quote} % which will only, of course, run Sage when necessary. % \begin{macrocode} """ Given a filename f, examines f.sagetex.sage and f.sagetex.sout and runs Sage if necessary. """ import hashlib import sys import os import re import subprocess import shutil import argparse def argparser(): p = argparse.ArgumentParser(description=__doc__.strip()) p.add_argument('--sage', action='store', default=find_sage(), help="Location of the Sage executable") p.add_argument('src', help="Input file name (basename or .sagetex.sage)") return p def find_sage(): return shutil.which('sage') or 'sage' def run(args): src = args.src path_to_sage = args.sage if src.endswith('.sagetex.sage'): src = src[:-13] else: src = os.path.splitext(src)[0] # Ensure results are output in the same directory as the source files os.chdir(os.path.dirname(src)) src = os.path.basename(src) usepackage = r'usepackage(\[.*\])?{sagetex}' uses_sagetex = False # If it does not use sagetex, obviously running sage is unnecessary. if os.path.isfile(src + '.tex'): with open(src + '.tex') as texf: for line in texf: if line.strip().startswith(r'\usepackage') and re.search(usepackage, line): uses_sagetex = True break else: # The .tex file might not exist if LaTeX output was put to a different # directory, so in that case just assume we need to build. uses_sagetex = True if not uses_sagetex: print(src + ".tex doesn't seem to use SageTeX, exiting.", file=sys.stderr) sys.exit(1) # if something goes wrong, assume we need to run Sage run_sage = True ignore = r"^( _st_.goboom|print\('SageT| ?_st_.current_tex_line)" try: with open(src + '.sagetex.sage', 'r') as sagef: h = hashlib.md5() for line in sagef: if not re.search(ignore, line): h.update(bytearray(line,'utf8')) except IOError: print('{0}.sagetex.sage not found, I think you need to typeset {0}.tex first.' ''.format(src), file=sys.stderr) sys.exit(1) try: with open(src + '.sagetex.sout', 'r') as outf: for line in outf: m = re.match('%([0-9a-f]+)% md5sum', line) if m: print('computed md5:', h.hexdigest()) print('sagetex.sout md5:', m.group(1)) if h.hexdigest() == m.group(1): run_sage = False break except IOError: pass if run_sage: print('Need to run Sage on {0}.'.format(src)) sys.exit(subprocess.call([path_to_sage, src + '.sagetex.sage'])) else: print('Not necessary to run Sage on {0}.'.format(src)) if __name__ == "__main__": run(argparser().parse_args()) % \end{macrocode} % % \subsection{\texttt{sagetex-makestatic}} % \label{sec:sagetex-makestatic} % \iffalse % %<*staticscript> % \fi % % Now the |sagetex-makestatic|: % % \begin{macrocode} """ Removes SageTeX macros from `inputfile' and replaces them with the Sage-computed results to make a "static" file. You'll need to have run Sage on `inputfile' already. `inputfile' can include the .tex extension or not. If you provide `outputfile', the results will be written to a file of that name. Specify `-o' or `--overwrite' to overwrite the file if it exists. See the SageTeX documentation for more details. """ import sys import time import os.path import argparse from sagetexparse import DeSageTex def argparser(): p = argparse.ArgumentParser(description=__doc__.strip()) p.add_argument('inputfile', help="Input file name (basename or .tex)") p.add_argument('outputfile', nargs='?', default=None, help="Output file name") p.add_argument('-o', '--overwrite', action="store_true", default=False, help="Overwrite output file if it exists") p.add_argument('-s', '--sout', action="store", default=None, help="Location of the .sagetex.sout file") return p def run(args): src, dst, overwrite = args.inputfile, args.outputfile, args.overwrite if dst is not None and (os.path.exists(dst) and not overwrite): print('Error: %s exists and overwrite option not specified.' % dst, file=sys.stderr) sys.exit(1) src, ext = os.path.splitext(src) texfn = src + '.tex' soutfn = args.sout if args.sout is not None else src + '.sagetex.sout' % \end{macrocode} % All the real work gets done in the line below. Sorry it's not more % exciting-looking. % \begin{macrocode} desagetexed = DeSageTex(texfn, soutfn) % \end{macrocode} % This part is cool: we need double percent signs at the beginning of % the line because Python needs them (so they get turned into single % percent signs) \emph{and} because \textsf{Docstrip} needs them (so the % line gets passed into the generated file). It's perfect! % \begin{macrocode} header = ("%% SageTeX commands have been automatically removed from this file and\n" "%% replaced with plain LaTeX. Processed %s.\n" "" % time.strftime('%a %d %b %Y %H:%M:%S', time.localtime())) if dst is not None: dest = open(dst, 'w') else: dest = sys.stdout dest.write(header) dest.write(desagetexed.result) if __name__ == "__main__": run(argparser().parse_args()) % \end{macrocode} % % \iffalse % %<*extractscript> % \fi % % \subsection{\texttt{sagetex-extract}} % % Same idea as |sagetex-makestatic|, except this does basically the opposite % thing. % \begin{macrocode} """ Extracts Sage code from `inputfile'. `inputfile' can include the .tex extension or not. If you provide `outputfile', the results will be written to a file of that name, otherwise the result will be printed to stdout. Specify `-o' or `--overwrite' to overwrite the file if it exists. See the SageTeX documentation for more details. """ import sys import time import os.path import argparse from sagetexparse import SageCodeExtractor def argparser(): p = argparse.ArgumentParser(description=__doc__.strip()) p.add_argument('inputfile', help="Input file name (basename or .tex)") p.add_argument('outputfile', nargs='?', default=None, help="Output file name") p.add_argument('-o', '--overwrite', action="store_true", default=False, help="Overwrite output file if it exists") p.add_argument('--no-inline', action="store_true", default=False, help="Extract code only from Sage environments") return p def run(args): src, dst, overwrite = args.inputfile, args.outputfile, args.overwrite if dst is not None and (os.path.exists(dst) and not overwrite): print('Error: %s exists and overwrite option not specified.' % dst, file=sys.stderr) sys.exit(1) src, ext = os.path.splitext(src) sagecode = SageCodeExtractor(src + '.tex', inline=not args.no_inline) header = ("#> This file contains Sage code extracted from %s%s.\n" "#> Processed %s.\n" "" % (src, ext, time.strftime('%a %d %b %Y %H:%M:%S', time.localtime()))) if dst is not None: dest = open(dst, 'w') else: dest = sys.stdout dest.write(header) dest.write(sagecode.result) if __name__ == "__main__": run(argparser().parse_args()) % \end{macrocode} % % \iffalse % %<*parsermod> % \fi % % \subsection{The parser module} % \changes{v2.2}{2009/06/17}{Update parser module to handle pause/unpause} % % Here's the module that does the actual parsing and replacing. It's % really quite simple, thanks to the awesome % \href{http://pyparsing.wikispaces.com}{Pyparsing module}. The parsing % code below is nearly self-documenting! Compare that to fancy regular % expressions, which sometimes look like someone sneezed punctuation all % over the screen. % \begin{macrocode} import sys import os import textwrap from pyparsing import * % \end{macrocode} % First, we define this very helpful parser: it finds the matching % bracket, and doesn't parse any of the intervening text. It's basically % like hitting the percent sign in Vim. This is useful for parsing \LTX % stuff, when you want to just grab everything enclosed by matching % brackets. % \begin{macrocode} def skipToMatching(opener, closer): nest = nestedExpr(opener, closer) return originalTextFor(nest) curlybrackets = skipToMatching('{', '}') squarebrackets = skipToMatching('[', ']') % \end{macrocode} % Next, parser for |\sage|, |\sageplot|, and pause/unpause calls: % \begin{macrocode} sagemacroparser = r'\sage' + curlybrackets('code') sagestrmacroparser = r'\sagestr' + curlybrackets('code') sageplotparser = (r'\sageplot' + Optional(squarebrackets)('opts') + Optional(squarebrackets)('format') + curlybrackets('code')) sagetexpause = Literal(r'\sagetexpause') sagetexunpause = Literal(r'\sagetexunpause') % \end{macrocode} % % With those defined, let's move on to our classes. % % \begin{macro}{SoutParser} % Here's the parser for the generated |.sout| file. The code below does % all the parsing of the |.sout| file and puts the results into a % list. Notice that it's on the order of 10 lines of code---hooray % for Pyparsing! % \begin{macrocode} class SoutParser(): def __init__(self, fn): self.label = [] % \end{macrocode} % A label line looks like % \begin{quote} % |\newlabel{@sageinline|\meta{integer}|}{|\marg{bunch of \LTX code}|{}{}{}{}}| % \end{quote} % which makes the parser definition below pretty obvious. We assign some % names to the interesting bits so the |newlabel| method can make the % \meta{integer} and \meta{bunch of \LTX code} into the keys and values % of a dictionary. The |DeSageTeX| class then uses that dictionary to % replace bits in the |.tex| file with their Sage-computed results. % \begin{macrocode} parselabel = (r'\newlabel{@sageinline' + Word(nums)('num') + '}{' + curlybrackets('result') + '{}{}{}{}}') % \end{macrocode} % We tell it to ignore comments, and hook up the list-making method. % \begin{macrocode} parselabel.ignore('%' + restOfLine) parselabel.setParseAction(self.newlabel) % \end{macrocode} % A |.sout| file consists of one or more such lines. Now go parse the % file we were given. % \begin{macrocode} try: OneOrMore(parselabel).parseFile(fn) except IOError: print('Error accessing {}; exiting. Does your .sout file exist?'.format(fn)) sys.exit(1) % \end{macrocode} % Pyparser's parse actions get called with three arguments: the string % that matched, the location of the beginning, and the resulting parse % object. Here we just add a new key-value pair to the dictionary, % remembering to strip off the enclosing brackets from the ``result'' % bit. % \begin{macrocode} def newlabel(self, s, l, t): self.label.append(t.result[1:-1]) % \end{macrocode} % \end{macro} % % \begin{macro}{DeSageTeX} % Now we define a parser for \LTX files that use \ST commands. We assume % that the provided |fn| is just a basename. % \begin{macrocode} class DeSageTex(): def __init__(self, texfn, soutfn): self.sagen = 0 self.plotn = 0 self.fn = os.path.basename(texfn) self.sout = SoutParser(soutfn) % \end{macrocode} % Parse |\sage| macros. We just need to pull in the result from the % |.sout| file and increment the counter---that's what |self.sage| does. % \begin{macrocode} strmacro = sagestrmacroparser smacro = sagemacroparser smacro.setParseAction(self.sage) strmacro.setParseAction(self.sage) % \end{macrocode} % Parse the |\usepackage{sagetex}| line. Right now we don't support % comma-separated lists of packages. % \begin{macrocode} usepackage = (r'\usepackage' + Optional(squarebrackets) + '{sagetex}') usepackage.setParseAction(replaceWith(r"""% "\usepackage{sagetex}" line was here: \RequirePackage{verbatim} \RequirePackage{graphicx} \newcommand{\sagetexpause}{\relax} \newcommand{\sagetexunpause}{\relax}""")) % \end{macrocode} % Parse |\sageplot| macros. % \begin{macrocode} splot = sageplotparser splot.setParseAction(self.plot) % \end{macrocode} % The printed environments (|sageblock| and |sageverbatim|) get turned % into |verbatim| environments. % \begin{macrocode} beginorend = oneOf('begin end') blockorverb = 'sage' + oneOf('block verbatim') blockorverb.setParseAction(replaceWith('verbatim')) senv = '\\' + beginorend + '{' + blockorverb + '}' % \end{macrocode} % The non-printed |sagesilent| environment gets commented out. We could % remove all the text, but this works and makes going back to \ST % commands (de-de-\ST{}ing?) easier. % \begin{macrocode} silent = Literal('sagesilent') silent.setParseAction(replaceWith('comment')) ssilent = '\\' + beginorend + '{' + silent + '}' % \end{macrocode} % The |\sagetexindent| macro is no longer relevant, so remove it from % the output (``suppress'', in Pyparsing terms). % \begin{macrocode} stexindent = Suppress(r'\setlength{\sagetexindent}' + curlybrackets) % \end{macrocode} % Now we define the parser that actually goes through the file. It just % looks for any one of the above bits, while ignoring anything that % should be ignored. % \begin{macrocode} doit = smacro | senv | ssilent | usepackage | splot | stexindent |strmacro doit.ignore('%' + restOfLine) doit.ignore(r'\begin{verbatim}' + SkipTo(r'\end{verbatim}')) doit.ignore(r'\begin{comment}' + SkipTo(r'\end{comment}')) doit.ignore(r'\sagetexpause' + SkipTo(r'\sagetexunpause')) % \end{macrocode} % We can't use the |parseFile| method, because that expects a ``complete % grammar'' in which everything falls into some piece of the parser. % Instead we suck in the whole file as a single string, and run % |transformString| on it, since that will just pick out the interesting % bits and munge them according to the above definitions. % \begin{macrocode} str = ''.join(open(texfn, 'r').readlines()) self.result = doit.transformString(str) % \end{macrocode} % That's the end of the class constructor, and it's all we need to do % here. You access the results of parsing via the |result| string. % % We do have two methods to define. The first does the same thing that % |\ref| does in your \LTX file: returns the content of the label and % increments a counter. % \begin{macrocode} def sage(self, s, l, t): self.sagen += 1 return self.sout.label[self.sagen - 1] % \end{macrocode} % The second method returns the appropriate |\includegraphics| command. % It does need to account for the default argument. % \begin{macrocode} def plot(self, s, l, t): self.plotn += 1 if len(t.opts) == 0: opts = r'[width=.75\textwidth]' else: opts = t.opts[0] return (r'\includegraphics%s{sage-plots-for-%s.tex/plot-%s}' % (opts, self.fn, self.plotn - 1)) % \end{macrocode} % \end{macro} % % \begin{macro}{SageCodeExtractor} % This class does the opposite of the first: instead of removing Sage % stuff and leaving only \LTX, this removes all the \LTX and leaves only % Sage. % \begin{macrocode} class SageCodeExtractor(): def __init__(self, texfn, inline=True): smacro = sagemacroparser smacro.setParseAction(self.macroout) splot = sageplotparser splot.setParseAction(self.plotout) % \end{macrocode} % Above, we used the general parsers for |\sage| and |\sageplot|. We % have to redo the environment parsers because it seems too hard to % define one parser object that will do both things we want: above, we % just wanted to change the environment name, and here we want to suck % out the code. Here, it's important that we find matching begin/end % pairs; above it wasn't. At any rate, it's not a big deal to redo this % parser. % \begin{macrocode} env_names = oneOf('sageblock sageverbatim sagesilent') senv = r'\begin{' + env_names('env') + '}' + SkipTo( r'\end{' + matchPreviousExpr(env_names) + '}')('code') senv.leaveWhitespace() senv.setParseAction(self.envout) spause = sagetexpause spause.setParseAction(self.pause) sunpause = sagetexunpause sunpause.setParseAction(self.unpause) if inline: doit = smacro | splot | senv | spause | sunpause else: doit = senv | spause | sunpause doit.ignore('%' + restOfLine) str = ''.join(open(texfn, 'r').readlines()) self.result = '' doit.transformString(str) def macroout(self, s, l, t): self.result += '#> \\sage{} from line %s\n' % lineno(l, s) self.result += textwrap.dedent(t.code[1:-1]) + '\n\n' def plotout(self, s, l, t): self.result += '#> \\sageplot{} from line %s:\n' % lineno(l, s) if t.format != '': self.result += '#> format: %s' % t.format[0][1:-1] + '\n' self.result += textwrap.dedent(t.code[1:-1]) + '\n\n' def envout(self, s, l, t): self.result += '#> %s environment from line %s:' % (t.env, lineno(l, s)) self.result += textwrap.dedent(''.join(t.code)) + '\n' def pause(self, s, l, t): self.result += ('#> SageTeX (probably) paused on input line %s.\n\n' % (lineno(l, s))) def unpause(self, s, l, t): self.result += ('#> SageTeX (probably) unpaused on input line %s.\n\n' % (lineno(l, s))) % \end{macrocode} % \end{macro} % \endinput % % Local Variables: % mode: doctex % TeX-master: "sagetex" % End: