Franck Pommereau

initial import

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()
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
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}