Showing
3 changed files
with
481 additions
and
0 deletions
pytex.py
0 → 100755
1 | +#!/usr/bin/env python | ||
2 | + | ||
3 | +import sys, os, os.path, argparse, inspect, ast, glob | ||
4 | +import SocketServer, socket, multiprocessing, threading | ||
5 | +import psutil | ||
6 | + | ||
7 | +try : | ||
8 | + import pygments, pygments.lexers, pygments.formatters | ||
9 | +except : | ||
10 | + pygments = None | ||
11 | + | ||
12 | +class Interpreter (object) : | ||
13 | + def __init__ (self, jobname) : | ||
14 | + self._t = [] | ||
15 | + if pygments is None : | ||
16 | + self.do_pygmentize = self._do_not_pygmentize | ||
17 | + self.base = jobname + "-pygments" | ||
18 | + self.do_reset() | ||
19 | + def do_reset (self) : | ||
20 | + self._t, self._g, self._l = [], {}, {} | ||
21 | + for name, method in inspect.getmembers(self, inspect.ismethod) : | ||
22 | + if name.startswith("do_") : | ||
23 | + self._g[name[3:]] = method | ||
24 | + if pygments : | ||
25 | + source = pygments.highlight( | ||
26 | + "pass", | ||
27 | + pygments.lexers.PythonLexer(), | ||
28 | + pygments.formatters.LatexFormatter(full=True, encoding="utf8")) | ||
29 | + with open(self.base + ".tex", "w") as out : | ||
30 | + for line in source.splitlines(True) : | ||
31 | + if line.strip().startswith(r"\documentclass{") : | ||
32 | + pass | ||
33 | + elif line.strip() == r"\begin{document}" : | ||
34 | + break | ||
35 | + elif line.strip() == r"\usepackage[utf8]{inputenc}" : | ||
36 | + pass | ||
37 | + else : | ||
38 | + out.write(line) | ||
39 | + self.do_tex(r"\input{%s.tex}" % self.base) | ||
40 | + def _do_not_pygmentize (self, path, include=True, inline=False) : | ||
41 | + outfile = os.path.splitext(path)[0] + ".tex" | ||
42 | + source = open(path).read() | ||
43 | + if inline : | ||
44 | + source = (r"\Verb$" | ||
45 | + + "".join(l.rstrip() for l in | ||
46 | + source.splitlines()).replace("$", r"$\Verb|$|\Verb$") | ||
47 | + + r"$\endinput") | ||
48 | + with open(outfile, "w") as out : | ||
49 | + out.write(source) | ||
50 | + if include : | ||
51 | + self.do_tex(r"\input{%s}" % outfile) | ||
52 | + def do_pygmentize (self, path, include=True, inline=False) : | ||
53 | + outfile = os.path.splitext(path)[0] + ".tex" | ||
54 | + source = pygments.highlight(open(path).read(), | ||
55 | + pygments.lexers.PythonLexer(), | ||
56 | + pygments.formatters.LatexFormatter()) | ||
57 | + if inline : | ||
58 | + lines = source.splitlines(True) | ||
59 | + lines[0] = r"\fvset{%s}\Verb$" % lines[0].split("[", 1)[-1].split("]", 1)[0] | ||
60 | + lines.pop(-1) | ||
61 | + source = "".join(l.rstrip() for l in lines) + r"$\endinput" | ||
62 | + with open(outfile, "w") as out : | ||
63 | + out.write(source) | ||
64 | + if include : | ||
65 | + self.do_tex(r"\input{%s}" % outfile) | ||
66 | + def do_makedef (self, name, path) : | ||
67 | + out = open(path + ".out").read().rsplit("\\", 1)[0] | ||
68 | + self.do_tex(r"\def\%s{%s}" % (name, self.do_escape(out))) | ||
69 | + def do_escape (self, text) : | ||
70 | + escape = {"\\" : "{\\textbackslash}", | ||
71 | + "^" : "{\\textasciicircum}", | ||
72 | + "~" : "{\\textasciitilde}"} | ||
73 | + return "".join(c if c not in "{}$%#&_\\^~" else escape.get(c, "\\" + c) | ||
74 | + for c in text) | ||
75 | + def _format (self, text, *args, **opt) : | ||
76 | + char = opt.pop("char", "@") | ||
77 | + if opt : | ||
78 | + raise ValueError("unexpected option %r" % iter(opt.keys()).next()) | ||
79 | + if not char : | ||
80 | + raise ValueError("empty separator") | ||
81 | + char2, clen, c2len = 2*char, len(char), 2*len(char) | ||
82 | + data = [] | ||
83 | + pos = 0 | ||
84 | + for i, c in enumerate(text) : | ||
85 | + if c[i:i+c2len] == char2 : | ||
86 | + data.append(char) | ||
87 | + elif c[i:i+clen] == char : | ||
88 | + data.append(self.do_escape(args[pos])) | ||
89 | + pos += 1 | ||
90 | + else : | ||
91 | + data.append(c) | ||
92 | + return "".join(data) | ||
93 | + def do_tex (self, text, *args, **opt) : | ||
94 | + if args or opt : | ||
95 | + self._t.append(self._format(text, *args, **opt)) | ||
96 | + else : | ||
97 | + self._t.append(text) | ||
98 | + def _clean (self, source) : | ||
99 | + lines = source.splitlines(True) | ||
100 | + first = lines[0] | ||
101 | + indent = len(first) - len(first.lstrip()) | ||
102 | + return "".join(l[indent:] for l in lines) | ||
103 | + def out (self, text) : | ||
104 | + with open(self.base + ".out", "w") as out : | ||
105 | + out.write(text + r"\endinput") | ||
106 | + def __call__ (self, path, mode) : | ||
107 | + self.path = path | ||
108 | + self.base = os.path.splitext(path)[0] | ||
109 | + source = self._clean(open(path).read()) | ||
110 | + self._t = [] | ||
111 | + if mode == "exec" : | ||
112 | + exec(source, self._g, self._l) | ||
113 | + self.out("".join(self._t)) | ||
114 | + elif mode == "eval" : | ||
115 | + out = eval(source, self._g, self._l) | ||
116 | + self.out("".join(self._t) + self.do_escape(str(out))) | ||
117 | + else : | ||
118 | + raise ValueError("invalid mode %r" % mode) | ||
119 | + | ||
120 | +class Handler (SocketServer.BaseRequestHandler) : | ||
121 | + def handle (self) : | ||
122 | + data, sock = self.request | ||
123 | + try : | ||
124 | + req = ast.literal_eval(data) | ||
125 | + except : | ||
126 | + sock.sendto(repr({"status" : "error", | ||
127 | + "message" : "cannot parse request"}), | ||
128 | + self.client_address) | ||
129 | + return | ||
130 | + if "method" not in req : | ||
131 | + sock.sendto(repr({"status" : "error", | ||
132 | + "message" : "no method given"}), | ||
133 | + self.client_address) | ||
134 | + return | ||
135 | + elif req["method"] not in ["exec", "eval", "quit"] : | ||
136 | + sock.sendto(repr({"status" : "error", | ||
137 | + "message" : "unknown method"}), | ||
138 | + self.client_address) | ||
139 | + return | ||
140 | + elif req["method"] == "quit" : | ||
141 | + sock.sendto(repr({"status" : "ok", | ||
142 | + "message" : "server is shutting down", | ||
143 | + "pid" : os.getpid()}), | ||
144 | + self.client_address) | ||
145 | + threading.Thread(target=self.server.shutdown).start() | ||
146 | + return | ||
147 | + elif "path" not in req : | ||
148 | + sock.sendto(repr({"status" : "error", | ||
149 | + "message" : "no path given"}), | ||
150 | + self.client_address) | ||
151 | + return | ||
152 | + elif not os.path.isfile(req["path"]) : | ||
153 | + sock.sendto(repr({"status" : "error", | ||
154 | + "message" : "file not found %r" % req["path"]}), | ||
155 | + self.client_address) | ||
156 | + return | ||
157 | + try : | ||
158 | + self.server.py(req["path"], req["method"]) | ||
159 | + except Exception as err : | ||
160 | + sock.sendto(repr({"status" : "error", | ||
161 | + "message" : "raised %s: %s" | ||
162 | + % (err.__class__.__name__, err)}), | ||
163 | + self.client_address) | ||
164 | + return | ||
165 | + sock.sendto(repr({"status" : "ok", | ||
166 | + "message" : "file proceeded successfully"}), | ||
167 | + self.client_address) | ||
168 | + | ||
169 | +class Server (multiprocessing.Process) : | ||
170 | + @classmethod | ||
171 | + def daemonize (cls, port, jobname) : | ||
172 | + child = cls(port, jobname) | ||
173 | + child.start() | ||
174 | + multiprocessing.process._current_process._children.discard(child) | ||
175 | + print("<server started at %s>" % child.pid) | ||
176 | + def __init__ (self, port, jobname) : | ||
177 | + multiprocessing.Process.__init__(self) | ||
178 | + self.port = port | ||
179 | + self.jobname = jobname | ||
180 | + def run (self) : | ||
181 | + self.server = SocketServer.UDPServer(("", self.port), Handler) | ||
182 | + self.py = Interpreter(self.jobname) | ||
183 | + self.server.serve_forever() | ||
184 | + @classmethod | ||
185 | + def quit (cls, port) : | ||
186 | + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | ||
187 | + sock.settimeout(1) | ||
188 | + sock.sendto(repr({"method": "quit"}), ("127.0.0.1", port)) | ||
189 | + return sock.recv(1024) | ||
190 | + @classmethod | ||
191 | + def process (cls, port, mode, path) : | ||
192 | + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | ||
193 | + sock.settimeout(1) | ||
194 | + sock.sendto(repr({"method": mode, "path" : path}), ("127.0.0.1", port)) | ||
195 | + return sock.recv(1024) | ||
196 | + | ||
197 | +def _getjobs (path) : | ||
198 | + return [l.strip().split(None, 1) for l in open(path) if l.strip()] | ||
199 | + | ||
200 | +def getjobs (jobname) : | ||
201 | + if os.path.exists(jobname + ".pytex") : | ||
202 | + return jobname, _getjobs(jobname + ".pytex") | ||
203 | + _jobname = os.path.splitext(jobname)[0] | ||
204 | + if os.path.exists(_jobname + ".pytex") : | ||
205 | + return jobname, _getjobs(_jobname + ".pytex") | ||
206 | + return jobname, None | ||
207 | + | ||
208 | +def main () : | ||
209 | + VERSION = "1.0" | ||
210 | + parser = argparse.ArgumentParser(prog="pytex", | ||
211 | + description="companion to pytex LaTeX package") | ||
212 | + parser.add_argument("-v", "--version", action="version", | ||
213 | + version="%%(prog)s %s" % VERSION) | ||
214 | + parser.add_argument("-b", "--base", action="store", default=None, | ||
215 | + dest="base", help="base DIR/NAME for temporary files") | ||
216 | + server = parser.add_mutually_exclusive_group() | ||
217 | + server.add_argument("--clean", action="store_true", default=False, | ||
218 | + dest="clean", help="remove all temporary and auxiliary files") | ||
219 | + server.add_argument("--listen", action="store", default=None, type=int, | ||
220 | + metavar="PORT", dest="listen", help="start server on PORT") | ||
221 | + server.add_argument("--call", action="store", default=None, type=int, | ||
222 | + metavar="PORT", dest="call", | ||
223 | + help="run file calling server on PORT") | ||
224 | + server.add_argument("--shutdown", action="store", default=None, type=int, | ||
225 | + metavar="PORT", dest="shutdown", help="shutdown server on PORT") | ||
226 | + parser.add_argument("args", nargs="*", metavar="ARG", | ||
227 | + help="LaTeX jobname (or other arguments depending on options)") | ||
228 | + args = parser.parse_args([a.strip("{}") for a in sys.argv[1:]]) | ||
229 | + if args.listen : | ||
230 | + if len(args.args) != 1 : | ||
231 | + print("usage: pytex [-b BASE] --listen PORT JOBNAME") | ||
232 | + sys.exit(1) | ||
233 | + Server.daemonize(args.listen, *args.args) | ||
234 | + return | ||
235 | + elif args.call : | ||
236 | + if len(args.args) != 2 : | ||
237 | + print("usage: pytex [-b BASE] --call PORT MODE FILE") | ||
238 | + sys.exit(1) | ||
239 | + elif args.args[0] not in ("eval", "exec") : | ||
240 | + print("pytex: expected 'exec' or 'eval', but got %r" % args.args[0]) | ||
241 | + sys.exit(1) | ||
242 | + print Server.process(args.call, *args.args) | ||
243 | + return | ||
244 | + elif args.shutdown : | ||
245 | + if len(args.args) > 0 : | ||
246 | + print("usage: pytex [-b BASE] --shutdown PORT") | ||
247 | + sys.exit(1) | ||
248 | + try : | ||
249 | + resp = Server.quit(args.shutdown) | ||
250 | + except socket.timeout : | ||
251 | + print("<could not reach server to shutdown>") | ||
252 | + sys.exit(1) | ||
253 | + try : | ||
254 | + resp = ast.literal_eval(resp) | ||
255 | + pid = int(resp["pid"]) | ||
256 | + except : | ||
257 | + print("<invalid answer from server>") | ||
258 | + sys.exit(1) | ||
259 | + child = psutil.Process(pid) | ||
260 | + try : | ||
261 | + child.wait(1) | ||
262 | + print("<server at %s has sutdown>" % child.pid) | ||
263 | + return | ||
264 | + except : | ||
265 | + child.terminate() | ||
266 | + try : | ||
267 | + child.wait(1) | ||
268 | + print("<server at %s terminated>" % child.pid) | ||
269 | + return | ||
270 | + except : | ||
271 | + child.kill() | ||
272 | + print("<server at %s killed>" % child.pid) | ||
273 | + return | ||
274 | + elif len(args.args) != 1 : | ||
275 | + print("usage: pytex [-b BASE] [--clean] JOBNAME") | ||
276 | + sys.exit(1) | ||
277 | + try : | ||
278 | + jobname, jobs = getjobs(args.args[0]) | ||
279 | + except : | ||
280 | + print("pytex: invalid file %r" % args.args[0]) | ||
281 | + sys.exit(2) | ||
282 | + if args.base is None : | ||
283 | + if jobs : | ||
284 | + first = jobs[0][1] | ||
285 | + args.base = first[:-4] | ||
286 | + else : | ||
287 | + args.base = jobname + ".pytmp" + os.sep | ||
288 | + jobs = [] | ||
289 | + if not os.path.isdir(jobname + ".pytmp") : | ||
290 | + os.mkdir(jobname + ".pytmp") | ||
291 | + if not os.path.exists(os.path.join(jobname + ".pytmp", jobname)) : | ||
292 | + open(os.path.join(jobname + ".pytmp", jobname), "w").close() | ||
293 | + if args.clean : | ||
294 | + for path in ([jobname + ".pytex", jobname + "-pygments.tex"] | ||
295 | + + glob.glob(args.base + "*.py") | ||
296 | + + glob.glob(args.base + "*.out") | ||
297 | + + glob.glob(args.base + "*.tex")) : | ||
298 | + try : | ||
299 | + os.unlink(path) | ||
300 | + except : | ||
301 | + pass | ||
302 | + return | ||
303 | + py = Interpreter(jobname) | ||
304 | + for mode, path in jobs : | ||
305 | + if not os.path.exists(path) : | ||
306 | + print("not found: %s" % path) | ||
307 | + elif mode in ("eval", "exec") : | ||
308 | + print("%s: %s" % (mode, path)) | ||
309 | + py(path, mode) | ||
310 | + elif mode == "code" : | ||
311 | + print("skip: %s" % path) | ||
312 | + else : | ||
313 | + print("pytex: invalid mode %r in file %r" % (mode, jobname + ".pytex")) | ||
314 | + | ||
315 | +if __name__ == "__main__" : | ||
316 | + main() |
pytex.sty
0 → 100644
1 | +\ProvidesPackage{pytex} | ||
2 | + | ||
3 | +\RequirePackage{pgfopts} | ||
4 | +\RequirePackage{sverb} | ||
5 | +\RequirePackage{fancyvrb} | ||
6 | + | ||
7 | +\makeatletter | ||
8 | + | ||
9 | +%% | ||
10 | +%% | ||
11 | + | ||
12 | +\edef\py@port{12345} | ||
13 | +\edef\py@base{\jobname.pytmp} | ||
14 | + | ||
15 | +\pgfkeys{ | ||
16 | + /pytex/.cd, | ||
17 | + base/.store in=\py@base, | ||
18 | + port/.store in=\py@port, | ||
19 | +} | ||
20 | + | ||
21 | +\ProcessPgfOptions{/pytex} | ||
22 | + | ||
23 | +\immediate\write18{pytex -b {\py@base} --listen {\py@port} {\jobname}} | ||
24 | +\AtEndDocument{\immediate\write18{pytex -b {\py@base} --quit {\py@port}}} | ||
25 | + | ||
26 | +\IfFileExists{\py@base/\jobname} | ||
27 | + {\global\edef\py@base{\py@base/}} | ||
28 | + {\global\edef\py@base{\py@base.}} | ||
29 | + | ||
30 | +%% | ||
31 | +%% | ||
32 | + | ||
33 | +\newcounter{py@count} | ||
34 | +\newwrite\py@file | ||
35 | +\newwrite\py@jobs | ||
36 | + | ||
37 | +\immediate\openout\py@jobs=\jobname.pytex | ||
38 | + | ||
39 | +\def\py@basename{\py@base\thepy@count} | ||
40 | + | ||
41 | +\def\py@next #1{% | ||
42 | + \edef\py@last{\py@basename}% | ||
43 | + \addtocounter{py@count}{1}% | ||
44 | + \edef\py@basename{\py@base\thepy@count}% | ||
45 | + \edef\py@src{\py@basename.py}% | ||
46 | + \edef\py@@src{{\py@src}}% | ||
47 | + \edef\py@out{{\py@basename.out}}% | ||
48 | + \edef\py@tex{{\py@basename.tex}}% | ||
49 | + \immediate\write\py@jobs{#1 \py@src}% | ||
50 | +} | ||
51 | + | ||
52 | +\def\py@open{\immediate\openout\py@file=\py@src} | ||
53 | +\def\py@write{\immediate\write\py@file} | ||
54 | +\def\py@close{\immediate\closeout\py@file} | ||
55 | +\def\py@compile #1{\immediate\write18{pytex -b {\py@base} --call {\py@port} #1 {\py@src}}} | ||
56 | +\def\py@input #1{\expandafter\expandafter\expandafter | ||
57 | + \InputIfFileExists\csname py@#1\endcsname{}{}} | ||
58 | + | ||
59 | +\def\pyv #1{% | ||
60 | + \py@next{eval}% | ||
61 | + \py@open | ||
62 | + \py@write{#1}% | ||
63 | + \py@close | ||
64 | + \py@compile{eval}% | ||
65 | + \py@input{out}% | ||
66 | +} | ||
67 | + | ||
68 | +\def\pyx #1{% | ||
69 | + \py@next{exec}% | ||
70 | + \py@open | ||
71 | + \py@write{#1}% | ||
72 | + \py@close | ||
73 | + \py@compile{exec}% | ||
74 | + \py@input{out}% | ||
75 | +} | ||
76 | + | ||
77 | +\def\pyc #1{\leavevmode | ||
78 | + \py@next{code}% | ||
79 | + \py@open | ||
80 | + \py@write{#1}% | ||
81 | + \py@close | ||
82 | + \pyx{pygmentize("\py@last.py", inline=True)}% | ||
83 | +} | ||
84 | + | ||
85 | +\def\pyd #1#2{% | ||
86 | + \py@next{eval}% | ||
87 | + \py@open | ||
88 | + \py@write{#2}% | ||
89 | + \py@close | ||
90 | + \py@compile{eval}% | ||
91 | + \pyx{makedef("#1", "\py@last")}% | ||
92 | +} | ||
93 | + | ||
94 | +\newenvironment{pyeval}{% | ||
95 | + \py@next{eval}% | ||
96 | + \expandafter\verbwrite\py@@src | ||
97 | +}{\endverbwrite | ||
98 | + \py@compile{eval}% | ||
99 | + \py@input{out}% | ||
100 | +} | ||
101 | + | ||
102 | +\newenvironment{pyexec}{% | ||
103 | + \py@next{exec}% | ||
104 | + \expandafter\verbwrite\py@@src | ||
105 | +}{\endverbwrite | ||
106 | + \py@compile{exec}% | ||
107 | + \py@input{out}% | ||
108 | +} | ||
109 | + | ||
110 | +\newenvironment{pycode}{% | ||
111 | + \py@next{exec}% | ||
112 | + \expandafter\verbwrite\py@@src | ||
113 | +}{\endverbwrite | ||
114 | + \pyx{pygmentize("\py@last.py", inline=False)} | ||
115 | +} | ||
116 | + | ||
117 | +%% | ||
118 | +%% | ||
119 | + | ||
120 | +\makeatother | ||
121 | + | ||
122 | +\pyx{reset()} | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
test.tex
0 → 100644
1 | +\documentclass{article} | ||
2 | + | ||
3 | +\usepackage{pytex} | ||
4 | +\parindent=0pt | ||
5 | + | ||
6 | +\begin{document} | ||
7 | + | ||
8 | +***\pyv{" ".join(s.capitalize() for s in "hello world!".split())}*** | ||
9 | + | ||
10 | +*\pyx{x=40}**\pyv{x+2}*** | ||
11 | + | ||
12 | +***\pyd{hello}{x+2}***\hello*** | ||
13 | + | ||
14 | +\begin{pyexec} | ||
15 | +def fact (n) : | ||
16 | + f = 1 | ||
17 | + for i in range(1, n+1) : | ||
18 | + f *= i | ||
19 | + return f | ||
20 | +\end{pyexec} | ||
21 | + | ||
22 | +\begin{pyeval} | ||
23 | +[fact(n) for n in range(10) | ||
24 | + if n % 2 == 0] | ||
25 | +\end{pyeval} | ||
26 | + | ||
27 | +********************** | ||
28 | + | ||
29 | +Code ``\pyc{lambda x : x + 1}'' is inlined. | ||
30 | + | ||
31 | +Code block: | ||
32 | +% | ||
33 | +\begin{pycode} | ||
34 | +def fact (n) : | ||
35 | + f = 1 | ||
36 | + for i in range(1, n+1) : | ||
37 | + f *= i | ||
38 | + return f | ||
39 | +\end{pycode} | ||
40 | + | ||
41 | +\IfFileExists{pytex.tmp}{found}{NO}. | ||
42 | + | ||
43 | +\end{document} |
-
Please register or login to post a comment