diff options
Diffstat (limited to 'Master/texmf-dist/source/latex/sagetex/remote-sagetex.dtx')
-rw-r--r-- | Master/texmf-dist/source/latex/sagetex/remote-sagetex.dtx | 438 |
1 files changed, 438 insertions, 0 deletions
diff --git a/Master/texmf-dist/source/latex/sagetex/remote-sagetex.dtx b/Master/texmf-dist/source/latex/sagetex/remote-sagetex.dtx new file mode 100644 index 00000000000..2360e220277 --- /dev/null +++ b/Master/texmf-dist/source/latex/sagetex/remote-sagetex.dtx @@ -0,0 +1,438 @@ +% \section{The \texttt{remote-sagetex} script} +% \label{sec:remote-sagetex-code} +% +% +% Here we describe the Python code for |remote-sagetex.py|. Since its +% job is to replicate the functionality of using Sage and |sagetex.py|, +% there is some overlap with the Python module. +% +% \iffalse +%<*remotesagetex> +% \fi +% +% The |#!/usr/bin/env python| line is provided for us by the |.ins| +% file's preamble, so we don't put it here. +% \begin{macrocode} +from __future__ import print_function +import json +import sys +import time +import re +import urllib +import hashlib +import os +import os.path +import shutil +import getopt +from contextlib import closing + +######################################################################### +# You can provide a filename here and the script will read your login # +# information from that file. The format must be: # +# # +# server = 'http://foo.com:8000' # +# username = 'my_name' # +# password = 's33krit' # +# # +# You can omit one or more of those lines, use " quotes, and put hash # +# marks at the beginning of a line for comments. Command-line args # +# take precedence over information from the file. # +######################################################################### +login_info_file = None # e.g. '/home/foo/Private/sagetex-login.txt' + + +usage = """Process a SageTeX-generated .sage file using a remote Sage server. + +Usage: {0} [options] inputfile.sage + +Options: + + -h, --help: print this message + -s, --server: the Sage server to contact + -u, --username: username on the server + -p, --password: your password + -f, --file: get login information from a file + +If the server does not begin with the four characters `http', then +`https://' will be prepended to the server name. + +You can hard-code the filename from which to read login information into +the remote-sagetex script. Command-line arguments take precedence over +the contents of that file. See the SageTeX documentation for formatting +details. + +If any of the server, username, and password are omitted, you will be +asked to provide them. + +See the SageTeX documentation for more details on usage and limitations +of remote-sagetex.""".format(sys.argv[0]) + +server, username, password = (None,) * 3 + +try: + opts, args = getopt.getopt(sys.argv[1:], 'hs:u:p:f:', + ['help', 'server=', 'user=', 'password=', 'file=']) +except getopt.GetoptError as err: + print(str(err), usage, sep='\n\n') + sys.exit(2) + +for o, a in opts: + if o in ('-h', '--help'): + print(usage) + sys.exit() + elif o in ('-s', '--server'): + server = a + elif o in ('-u', '--user'): + username = a + elif o in ('-p', '--password'): + password = a + elif o in ('-f', '--file'): + login_info_file = a + +if len(args) != 1: + print('Error: must specify exactly one file. Please specify options first.', + usage, sep='\n\n') + sys.exit(2) + +jobname = os.path.splitext(args[0])[0] +% \end{macrocode} +% When we send things to the server, we get everything back as a string, +% including tracebacks. We can search through output using regexps to +% look for typical traceback strings, but there's a more robust way: put +% in a special string that changes every time and is printed when +% there's an error, and look for that. Then it is massively unlikely +% that a user's code could produce output that we'll mistake for an +% actual traceback. System time will work well enough for these +% purposes. We produce this string now, and we it when parsing the +% |.sage| file (we insert it into code blocks) and when parsing the +% output that the remote server gives us. +% \begin{macrocode} +traceback_str = 'Exception in SageTeX session {0}:'.format(time.time()) +% \end{macrocode} +% \begin{macro}{parsedotsage} +% To figure out what commands to send the remote server, we actually +% read in the |.sage| file as strings and parse it. This seems a bit +% strange, but since we know exactly what the format of that file is, we +% can parse it with a couple flags and a handful of regexps. +% \begin{macrocode} +def parsedotsage(fn): + with open(fn, 'r') as f: +% \end{macrocode} +% Here are the regexps we use to snarf the interesting bits out of the +% |.sage| file. Below we'll use the |re| module's |match| function so we +% needn't anchor any of these at the beginning of the line. +% \begin{macrocode} + inline = re.compile(r" _st_.inline\((?P<num>\d+), (?P<code>.*)\)") + plot = re.compile(r" _st_.plot\((?P<num>\d+), (?P<code>.*)\)") + goboom = re.compile(r" _st_.goboom\((?P<num>\d+)\)") + pausemsg = re.compile(r"print.'(?P<msg>SageTeX (un)?paused.*)'") + blockbegin = re.compile(r"_st_.blockbegin\(\)") + ignore = re.compile(r"(try:)|(except):") + in_comment = False + in_block = False + cmds = [] +% \end{macrocode} +% Okay, let's go through the file. We're going to make a list of +% dictionaries. Each dictionary corresponds to something we have to do +% with the remote server, except for the pause/unpause ones, which we +% only use to print out information for the user. All the dictionaries +% have a |type| key, which obviously tells you type they are. The +% pause/unpause dictionaries then just have a |msg| which we toss out to +% the user. The ``real'' dictionaries all have the following keys: +% \begin{itemize} +% \item |type|: one of |inline|, |plot|, and |block|. +% \item |goboom|: used to help the user pinpoint errors, just like the +% |goboom| function (page \pageref{macro:goboom}) does. +% \item |code|: the code to be executed. +% \end{itemize} +% Additionally, the |inline| and |plot| dicts have a |num| key for the +% label we write to the |.sout| file. +% +% Here's the whole parser loop. The interesting bits are for parsing +% blocks because there we need to accumulate several lines of code. +% \begin{macrocode} + for line in f.readlines(): + if line.startswith('"""'): + in_comment = not in_comment + elif not in_comment: + m = pausemsg.match(line) + if m: + cmds.append({'type': 'pause', + 'msg': m.group('msg')}) + m = inline.match(line) + if m: + cmds.append({'type': 'inline', + 'num': m.group('num'), + 'code': m.group('code')}) + m = plot.match(line) + if m: + cmds.append({'type': 'plot', + 'num': m.group('num'), + 'code': m.group('code')}) +% \end{macrocode} +% The order of the next three ``if''s is important, since we need the +% ``goboom'' line and the ``blockbegin'' line to \emph{not} get included +% into the block's code. Note that the lines in the |.sage| file already +% have some indentation, which we'll use when sending the block to the +% server---we wrap the text in a try/except. +% \begin{macrocode} + m = goboom.match(line) + if m: + cmds[-1]['goboom'] = m.group('num') + if in_block: + in_block = False + if in_block and not ignore.match(line): + cmds[-1]['code'] += line + if blockbegin.match(line): + cmds.append({'type': 'block', + 'code': ''}) + in_block = True + return cmds +% \end{macrocode} +% \end{macro} +% Parsing the |.sage| file is simple enough so that we can write one +% function and just do it. Interacting with the remote server is a bit +% more complicated, and requires us to carry some state, so let's make a +% class. +% +% \begin{macro}{RemoteSage} +% \changes{v2.2.1}{2009/06/20}{Fix stupid bug in \texttt{do\_inline()} +% so that we actually write output to .sout file} +% +% This is pretty simple; it's more or less a translation of the examples +% in \texttt{sage/server/simple/twist.py}. +% \begin{macrocode} +debug = False +class RemoteSage: + def __init__(self, server, user, password): + self._srv = server.rstrip('/') + sep = '___S_A_G_E___' + self._response = re.compile('(?P<header>.*)' + sep + + '\n*(?P<output>.*)', re.DOTALL) + self._404 = re.compile('404 Not Found') + self._session = self._get_url('login', + urllib.urlencode({'username': user, + 'password': + password}))['session'] +% \end{macrocode} +% In the string below, we want to do ``partial formatting'': we format +% in the traceback string now, and want to be able to format in the code +% later. The double braces get ignored by |format()| now, and are picked +% up by |format()| when we use this later. +% \begin{macrocode} + self._codewrap = """try: +{{0}} +except: + print('{0}') + traceback.print_exc()""".format(traceback_str) + self.do_block(""" + import traceback + def __st_plot__(counter, _p_, format='notprovided', **kwargs): + if format == 'notprovided': + formats = ['eps', 'pdf'] + else: + formats = [format] + for fmt in formats: + plotfilename = 'plot-%s.%s' % (counter, fmt) + _p_.save(filename=plotfilename, **kwargs)""") + + def _encode(self, d): + return 'session={0}&'.format(self._session) + urllib.urlencode(d) + + def _get_url(self, action, u): + with closing(urllib.urlopen(self._srv + '/simple/' + action + + '?' + u)) as h: + data = self._response.match(h.read()) + result = json.loads(data.group('header')) + result['output'] = data.group('output').rstrip() + return result + + def _get_file(self, fn, cell, ofn=None): + with closing(urllib.urlopen(self._srv + '/simple/' + 'file' + '?' + + self._encode({'cell': cell, 'file': fn}))) as h: + myfn = ofn if ofn else fn + data = h.read() + if not self._404.search(data): + with open(myfn, 'w') as f: + f.write(data) + else: + print('Remote server reported {0} could not be found:'.format( + fn)) + print(data) +% \end{macrocode} +% The |code| below gets stuffed between a try/except, so make sure it's +% indented! +% \begin{macrocode} + def _do_cell(self, code): + realcode = self._codewrap.format(code) + result = self._get_url('compute', self._encode({'code': realcode})) + if result['status'] == 'computing': + cell = result['cell_id'] + while result['status'] == 'computing': + sys.stdout.write('working...') + sys.stdout.flush() + time.sleep(10) + result = self._get_url('status', self._encode({'cell': cell})) + if debug: + print('cell: <<<', realcode, '>>>', 'result: <<<', + result['output'], '>>>', sep='\n') + return result + + def do_inline(self, code): + return self._do_cell(' print(latex({0}))'.format(code)) + + def do_block(self, code): + result = self._do_cell(code) + for fn in result['files']: + self._get_file(fn, result['cell_id']) + return result + + def do_plot(self, num, code, plotdir): + result = self._do_cell(' __st_plot__({0}, {1})'.format(num, code)) + for fn in result['files']: + self._get_file(fn, result['cell_id'], os.path.join(plotdir, fn)) + return result +% \end{macrocode} +% When using the simple server API, it's important to log out so the +% server doesn't accumulate idle sessions that take up lots of memory. +% We define a |close()| method and use this class with the |closing| +% context manager that always calls |close()| on the way out. +% \begin{macrocode} + def close(self): + sys.stdout.write('Logging out of {0}...'.format(server)) + sys.stdout.flush() + self._get_url('logout', self._encode({})) + print('done') +% \end{macrocode} +% \end{macro} +% Next we have a little pile of miscellaneous functions and variables +% that we want to have at hand while doing our work. Note that we again +% use the traceback string in the error-finding regular expression. +% \begin{macrocode} +def do_plot_setup(plotdir): + printc('initializing plots directory...') + if os.path.isdir(plotdir): + shutil.rmtree(plotdir) + os.mkdir(plotdir) + return True + +did_plot_setup = False +plotdir = 'sage-plots-for-' + jobname + '.tex' + +def labelline(n, s): + return r'\newlabel{@sageinline' + str(n) + '}{{' + s + '}{}{}{}{}}\n' + +def printc(s): + print(s, end='') + sys.stdout.flush() + +error = re.compile("(^" + traceback_str + ")|(^Syntax Error:)", re.MULTILINE) + +def check_for_error(string, line): + if error.search(string): + print(""" +**** Error in Sage code on line {0} of {1}.tex! +{2} +**** Running Sage on {1}.sage failed! Fix {1}.tex and try again.""".format( + line, jobname, string)) + sys.exit(1) +% \end{macrocode} +% Now let's actually start doing stuff. +% \begin{macrocode} +print('Processing Sage code for {0}.tex using remote Sage server.'.format( + jobname)) + +if login_info_file: + with open(login_info_file, 'r') as f: + print('Reading login information from {0}.'.format(login_info_file)) + get_val = lambda x: x.split('=')[1].strip().strip('\'"') + for line in f: + print(line) + if not line.startswith('#'): + if line.startswith('server') and not server: + server = get_val(line) + if line.startswith('username') and not username: + username = get_val(line) + if line.startswith('password') and not password: + password = get_val(line) + +if not server: + server = raw_input('Enter server: ') + +if not server.startswith('http'): + server = 'https://' + server + +if not username: + username = raw_input('Enter username: ') + +if not password: + from getpass import getpass + password = getpass('Please enter password for user {0} on {1}: '.format( + username, server)) + +printc('Parsing {0}.sage...'.format(jobname)) +cmds = parsedotsage(jobname + '.sage') +print('done.') + +sout = '% This file was *autogenerated* from the file {0}.sage.\n'.format( + os.path.splitext(jobname)[0]) + +printc('Logging into {0} and starting session...'.format(server)) +with closing(RemoteSage(server, username, password)) as sage: + print('done.') + for cmd in cmds: + if cmd['type'] == 'inline': + printc('Inline formula {0}...'.format(cmd['num'])) + result = sage.do_inline(cmd['code']) + check_for_error(result['output'], cmd['goboom']) + sout += labelline(cmd['num'], result['output']) + print('done.') + if cmd['type'] == 'block': + printc('Code block begin...') + result = sage.do_block(cmd['code']) + check_for_error(result['output'], cmd['goboom']) + print('end.') + if cmd['type'] == 'plot': + printc('Plot {0}...'.format(cmd['num'])) + if not did_plot_setup: + did_plot_setup = do_plot_setup(plotdir) + result = sage.do_plot(cmd['num'], cmd['code'], plotdir) + check_for_error(result['output'], cmd['goboom']) + print('done.') + if cmd['type'] == 'pause': + print(cmd['msg']) + if int(time.time()) % 2280 == 0: + printc('Unscheduled offworld activation; closing iris...') + time.sleep(1) + print('end.') + +with open(jobname + '.sage', 'r') as sagef: + h = hashlib.md5() + for line in sagef: + if (not line.startswith(' _st_.goboom') and + not line.startswith("print 'SageT")): + h.update(line) +% \end{macrocode} +% Putting the |{1}| in the string, just to replace it with |%|, seems a +% bit weird, but if I put a single percent sign there, Docstrip won't +% put that line into the resulting |.py| file---and if I put two percent +% signs, it replaces them with |\MetaPrefix| which is |##| when this +% file is generated. This is a quick and easy workaround. +% \begin{macrocode} + sout += """%{0}% md5sum of corresponding .sage file +{1} (minus "goboom" and pause/unpause lines) +""".format(h.hexdigest(), '%') + +printc('Writing .sout file...') +with open(jobname + '.sout', 'w') as soutf: + soutf.write(sout) + print('done.') +print('Sage processing complete. Run LaTeX on {0}.tex again.'.format(jobname)) +% \end{macrocode} + +% \endinput +%</remotesagetex> +% Local Variables: +% mode: doctex +% TeX-master: "sagetexpackage" +% End: |