Initial version 0.1
[pytex.git] / pytex.dtx
1 % \def\fileversion{0.1}
2 % \def\filedate{2010/03/03}
3 % \iffalse meta-comment
4 %<package>\def\fileversion{0.1}
5 %<package>\def\filedate{2010/03/03}
6 %
7 % Copyright (C) 2010  W. Trevor King
8 % -------------------------------------------------------
9
10 % This file may be distributed and/or modified under the
11 % conditions of the LaTeX Project Public License, either version 1.3
12 % of this license or (at your option) any later version.
13 % The latest version of this license is in:
14 %
15 %    http://www.latex-project.org/lppl.txt
16 %
17 % and version 1.3 or later is part of all distributions of LaTeX 
18 % version 2003/12/01 or later.
19 %
20 % Docstrip formatting initially based on Scott Pakin's
21 % dtxtut/cskeleton.dtx
22 %   http://www.ctan.org/tex-archive/info/dtxtut/
23 % Shellout calls initially based on Will Robertson's asyfig.sty
24 %   http://github.com/wspr/asyfig/
25 %
26 % \fi
27 %
28 % \iffalse meta-comment
29 %<*driver>
30 \ProvidesFile{pytex.dtx}
31 %</driver>
32 %<package>\NeedsTeXFormat{LaTeX2e}
33 %<package>\ProvidesClass{pytex}
34 %<package> [\filedate\ \fileversion\ embed Python in LaTeX files]
35 %
36 % Code for the .ini driver, see section "2.1 The driver file" of doc.dtx.
37 %<*driver>
38 \documentclass{ltxdoc}
39 \usepackage[colorlinks=true]{hyperref}
40 %</driver>
41 % Include some useful commands from |xkeyval|'s |<preamble>|.
42 %<*driver>
43 \usepackage{xcolor}
44 \usepackage{listings}
45 \lstnewenvironment{command}{%
46   \lstset{columns=flexible,frame=single,backgroundcolor=\color{blue!20},%
47     xleftmargin=\fboxsep,xrightmargin=\fboxsep,escapeinside=`',gobble=1}}{}
48 \lstnewenvironment{example}{%
49   \lstset{basicstyle=\footnotesize\ttfamily,columns=flexible,frame=single,%
50     backgroundcolor=\color{yellow!20},xleftmargin=\fboxsep,%
51     xrightmargin=\fboxsep,gobble=1}}{}
52 %</driver>
53 % Define a quick and dirty version of |xkeyval|'s |\DescribeOptions|.
54 %<*driver>
55 \newenvironment{option}[1]{\begin{macro}{#1}}
56                           {\end{macro}}
57 %</driver>
58 % Some commonly used abbreviations from |classes.dtx|.
59 %<*driver>
60 \newcommand*{\Lopt}[1]{\textsf {#1}}
61 \newcommand*{\file}[1]{\texttt {#1}}
62 \newcommand*{\Lcount}[1]{\textsl {\small#1}}
63 \newcommand*{\pstyle}[1]{\textsl {#1}}
64 %</driver>
65 %<*driver>
66 \makeatletter
67 \def\DescribeOption#1{\leavevmode\@bsphack
68               \marginpar{\raggedleft\PrintDescribeOption{#1}}%
69               \SpecialOptionIndex{#1}\@esphack\ignorespaces}
70 \def\PrintDescribeOption#1{\strut\emph{option}\\\MacroFont #1\ }
71 \def\SpecialOptionIndex#1{\@bsphack
72     \index{#1\actualchar{\protect\ttfamily#1}
73            (option)\encapchar usage}%
74     \index{options:\levelchar#1\actualchar{\protect\ttfamily#1}\encapchar
75            usage}\@esphack}
76 \def\DescribeOptions#1{\leavevmode\@bsphack
77   \marginpar{\raggedleft\strut\emph{options}%
78   \@for\@tempa:=#1\do{%
79     \\\strut\MacroFont\@tempa\SpecialOptionIndex\@tempa
80   }}\@esphack\ignorespaces}
81 \makeatother
82 %</driver>
83 %<*driver>
84 \EnableCrossrefs
85 \RecordChanges
86 \EnableCrossrefs         
87 \begin{document}
88   \DocInput{pytex.dtx}
89 \end{document}
90 %</driver>
91 % \fi
92 %
93 % \CheckSum{0}
94 %
95 % \CharacterTable
96 %  {Upper-case    \A\B\C\D\E\F\G\H\I\J\K\L\M\N\O\P\Q\R\S\T\U\V\W\X\Y\Z
97 %   Lower-case    \a\b\c\d\e\f\g\h\i\j\k\l\m\n\o\p\q\r\s\t\u\v\w\x\y\z
98 %   Digits        \0\1\2\3\4\5\6\7\8\9
99 %   Exclamation   \!     Double quote  \"     Hash (number) \#
100 %   Dollar        \$     Percent       \%     Ampersand     \&
101 %   Acute accent  \'     Left paren    \(     Right paren   \)
102 %   Asterisk      \*     Plus          \+     Comma         \,
103 %   Minus         \-     Point         \.     Solidus       \/
104 %   Colon         \:     Semicolon     \;     Less than     \<
105 %   Equals        \=     Greater than  \>     Question mark \?
106 %   Commercial at \@     Left bracket  \[     Backslash     \\
107 %   Right bracket \]     Circumflex    \^     Underscore    \_
108 %   Grave accent  \`     Left brace    \{     Vertical bar  \|
109 %   Right brace   \}     Tilde         \~}
110 %
111 %
112 % \changes{0.1}{2006/01/18}{Initial package}
113 %
114 % \MakeShortVerb{\|}
115 % \newcommand{\pkg}[1]{\textsf{#1}}
116 % \newcommand{\cls}[1]{\textsf{#1}}
117 % \newcommand{\fixme}[1]{\emph{FIXME: #1}}
118 % \newcommand{\python}{Python}
119 % \newcommand{\firstPython}{\href{http://www.python.org/}{\python}}
120
121 % \title{The \textsf{pytex} class\thanks{This document
122 %   corresponds to \textsf{pytex}~\fileversion, dated \filedate.}}
123 % \author{W.~Trevor King \\\texttt{wking@drexel.edu}}
124 % \date{\filedate}
125 %
126 % \maketitle
127 %
128 %
129 % \section{Introduction}
130 %
131 % This package defines a \LaTeX\ package for embedding \firstPython\ in
132 % \LaTeX\ files.  This is useful, for example, if you want to automate
133 % physics problem generation.  This is intended for easy transitions
134 % from static \LaTeX sources, or for those to whom \LaTeX seems more
135 % natural.  An alternative approach would be to write \python\ code
136 % that built static \LaTeX\ files on the fly.
137 %
138 %
139 % \section{Usage}
140 %
141 % Include \pkg{pytex} in your preamble (after |\documentclass{}| and
142 % before |\begin{document}|).
143 % \begin{example}
144 %   \usepackage{pytex}
145 % \end{example}
146 %
147 % This will give you the |\pytex{}| and |\pytexfile{}| macros.
148 % The long names are chosen to reduce the risk of collision with
149 % other packages, but feel free to abbreviate.
150 % \begin{example}
151 %   \newcommand{\py}[1]{\pytex{#1}}
152 % \end{example}
153 %
154 % Remember to use |-shell-escape| option or its equivalent for your
155 % flavor of \LaTeX, since \pkg{pytex} works by shelling out to
156 % \python.  Obviously, you'll have to trust the user writing whatever
157 % goes into the |\pytex| macro because (1) no shell escaping is done
158 % on the argument and (2) even if it were, you can do whatever you
159 % want through \python's |os.system| etc.
160 %
161 %
162 % \subsection{Macros}
163 %
164 % \begin{command}
165 %   `\cs{pytexfile}\marg{path}'
166 % \end{command}
167 % \DescribeMacro{\pytexfile}
168 % Set the working pickle file to \meta{path}.  \pkg{pytex} keeps track
169 % of your environment by
170 % \href{http://docs.python.org/library/pickle.html}{pickling}
171 % everything in a pickle file.  By default, this will be
172 % |./\jobname.pkl|, but feel free to use whatever name you want, or to
173 % switch back an forth between names.
174 % \begin{example}
175 %   \pytexfile{problem1}
176 % \end{example}
177 % The pickle file is \emph{not} cleared at the start of each
178 % \LaTeX\ run (but it could be, perhaps via a |\newpytexfile{}|).
179 %
180 % \begin{command}
181 %   `\cs{pytex}\marg{python-code}'
182 % \end{command}
183 % \DescribeMacro{\pytex}
184 % Here's where the fun begins.
185 % \begin{example}
186 %   If $a=\pytex{p.a=1; print p.a}$ and $b=\pytex{p.b=2; print p.b}$,
187 %   then $a\cdot b = \pytex{print p.a*p.b}$.
188 % \end{example}
189 % |p| is a |PyTeX_Storage| instance that handles saving to and loading
190 % from the current pytex file.
191 %
192 %
193 % \section{Implementation}
194 %
195 %    \begin{macrocode}
196 %<*package>
197 %    \end{macrocode}
198 %
199 % Use \pkg{ifplatform} to detect Linux / Mac / MS Windows \ldots.
200 %    \begin{macrocode}
201 \RequirePackage{ifplatform}
202 %    \end{macrocode}
203 %
204 % Use \pkg{catchfile} for reading in files.
205 %    \begin{macrocode}
206 \RequirePackage{catchfile}
207 %    \end{macrocode}
208 %
209 % \begin{macro}{\pytex@currentfile}
210 % Keep track of the current pytex file, making |\jobname| the default.
211 %    \begin{macrocode}
212 \newcommand\pytex@currentfile{\jobname}
213 %    \end{macrocode}
214 % \end{macro}
215 %
216 % \begin{macro}{\pytexfile}
217 % Give the user a way to change the current pytex file.
218 %    \begin{macrocode}
219 \newcommand\pytexfile[1]{\renewcommand\pytex@currentfile{#1}}
220 %    \end{macrocode}
221 % \end{macro}
222 %
223 % \begin{macro}{\pytex@stdout}
224 % \begin{macro}{\pytex@stderr}
225 % Temporary files for capturing stdout and stderr from a |\pytex|
226 % call.
227 %    \begin{macrocode}
228 \newcommand\pytex@stdout{pytex.stdout}
229 \newcommand\pytex@stderr{pytex.stderr}
230 %    \end{macrocode}
231 % \end{macro}
232 % \end{macro}
233 %
234 % \begin{macro}{\pytex@par}
235 % We will be using |^^J| for line breaks on our input stderr to
236 % preserve line wrapping.  Here we give |^^J| a name to make
237 % the stderr comparison more clear.
238 %    \begin{macrocode}
239 \newcommand\pytex@par{^^J}
240 %    \end{macrocode}
241 % \end{macro}
242 %
243 % \begin{macro}{\pytex@qt}
244 % Surround an argument with escaped quotes.
245 % For wrapping strings in the python shellout.
246 % \fixme{doesn't actually work.}
247 % ^^A echo '\write16{"hi \"there"}\bye' | tex
248 % ^^A echo '\write16{"hi \ "there"}\bye' | tex
249 %    \begin{macrocode}
250 \newcommand{\pytex@qt}[1]{\char"22 #1\char"22}
251 %    \end{macrocode}
252 % \end{macro}
253 %
254 % \begin{macro}{\pytex}
255 % Here's the meat.  Use the pytex file to provide a persitent
256 % environment for \python\ execution.
257 %    \begin{macrocode}
258 \newcommand\pytex[1]{%
259   \immediate\write18{%
260     python -c "import pytex;
261                p = pytex.PyTeX_Storage('\pytex@currentfile');
262                p.load();
263                #1;
264                p.save();"
265       1> \pytex@stdout\space 2> \pytex@stderr}%
266 %    \end{macrocode}
267 % Read stdout and stderr files into |\@tempa| and |\@tempb|
268 % respectively.  \fixme{How do we know the command is complete?  wait
269 % call?}.  We preserve linebreaks while reading stderr so that
270 % they will show up if we have to |\typeout| them later.
271 %    \begin{macrocode}
272   \CatchFileEdef{\@tempa}{\pytex@stdout}{}%
273   \CatchFileEdef{\@tempb}{\pytex@stderr}{\let\par\pytex@par\obeylines}%
274 %    \end{macrocode}
275 % Remove the temporary files.
276 %    \begin{macrocode}
277   \immediate\write18{\ifwindows del \else rm \fi \pytex@stdout}
278   \immediate\write18{\ifwindows del \else rm \fi \pytex@stderr}
279 %    \end{macrocode}
280 % If stderr is blank (equivalent to a single endline?)\ldots
281 %    \begin{macrocode}
282   \if\@tempb\pytex@par
283 %    \end{macrocode}
284 % \ldots the \python\ code ran cleanly.  Put stdout in the token
285 % stream.
286 %    \begin{macrocode}
287     \@tempa
288   \else
289 %    \end{macrocode}
290 % \ldots otherwise, the \python\ code failed.  Put stderr in the token
291 % stream and raise an error.
292 %    \begin{macrocode}
293      \errorstopmode
294      \typeout{%
295        ------------------------------------^^J%
296        ----------- PYTHON ERROR -----------^^J}
297      %\typeout{\expandafter\strip@prefix\meaning\@tempb}
298      \typeout{\@tempb}
299      \typeout{^^J%
300        ----------- PYTHON ERROR -----------^^J%
301        ------------------------------------}%
302      \batchmode
303      \end{document}
304   \fi
305 }
306 %    \end{macrocode}
307 % \end{macro}
308 %
309 %    \begin{macrocode}
310 %</package>
311 %    \end{macrocode}
312 %
313 %
314 % \section{Module}
315 % \label{sec:module}
316 %
317 %    \begin{macrocode}
318 %<*module>
319 # See pytex.pdf for details.
320
321 import os
322 import pickle
323
324 class PyTeX_Storage (object):
325     """
326     >>> test_file = 'test.pkl'
327     >>> p = PyTeX_Storage(test_file)
328     >>> p.a = 1
329     >>> p.b = 2
330     >>> p.save()
331     >>> del p
332     >>> p = PyTeX_Storage(test_file)
333     >>> p.a
334     Traceback (most recent call last):
335       ...
336     AttributeError: 'PyTeX_Storage' object has no attribute 'a'
337     >>> p.load()
338     >>> p.a
339     1
340     >>> p.b
341     2
342     >>> p.cleanup()
343     """
344     def __init__(self, pytexfile):
345         self._pytexfile = self._pytexfile_path(pytexfile)
346         self._hidden = dir(self)
347         self._hidden.append('_hidden')
348
349     def _pytexfile_path(self, pytexfile):
350         """
351         >>> p = PyTeX_Storage('dummy')
352         >>> p._pytexfile_path('test')
353         'test.pkl'
354         >>> p._pytexfile_path('test.pkl')
355         'test.pkl'
356         """
357         if not pytexfile.endswith('.pkl'):
358             pytexfile += '.pkl'
359         return pytexfile
360     
361     def load(self):
362         try:
363             f = open(self._pytexfile, 'r')
364         except IOError: # [Errno 2] No such file or directory
365             return
366         saved = pickle.load(f)
367         for key,value in saved.items():
368             setattr(self, key, value)
369     
370     def save(self, protocol=-1):
371         saved = {}
372         for key in dir(self):
373             if key in self._hidden:
374                 continue
375             saved[key] = getattr(self, key)
376         f = open(self._pytexfile, 'w')
377         pickle.dump(saved, f, protocol=protocol)
378         f.close()
379
380     def cleanup(self):
381         os.remove(self._pytexfile)
382
383 def test() :
384     import doctest
385     doctest.testmod()
386
387 if __name__ == '__main__':
388     test()
389 %</module>
390 %    \end{macrocode}
391 %
392 %
393 % \section{Examples}
394 % \label{sec:example}
395 %
396 %     \begin{macrocode}
397 %<*example>
398 %% See pytex.pdf for details.
399 \documentclass{article}
400 \usepackage{pytex}
401 \begin{document}
402
403 \section{Problem 1}
404 \pytexfile{problem1}
405
406 If $a=\pytex{p.a=1; print p.a}$ and $b=\pytex{p.b=2; print p.b}$, then
407 what is $a\cdot b$? [Answer: $\pytex{p.ans=p.a*p.b; print p.ans}$]
408
409 \section{Problem 2}
410 \pytexfile{problem2}
411
412 If $a=\pytex{p.a=1; print p.a}$ and $b=\pytex{p.b=3; print p.b}$, then
413 what is $a\cdot b$? [Answer: $\pytex{p.ans=p.a*p.b; print p.ans}$]
414
415 \section{Discussion}
416 \pytexfile{problem3}
417
418 Comparing our answers for Problems 1 and 2, we see that the answer to
419 1 (\pytexfile{problem1}$\pytex{print p.ans}$) is smaller than the answer
420 to 2 (\pytexfile{problem2}$\pytex{print p.ans}$).  This is to be expected,
421 since $a$ is the same in both problems, and $b$ is larger in Problem 2
422 ($\pytexfile{problem1}\pytex{print p.b} <
423   \pytexfile{problem2}\pytex{print p.b}$).
424 %    \end{macrocode}
425 % We can explicitly check our statement.  \LaTeX\ compilation will
426 % fail if the |\pytex| call returns a non-zero status code, as it
427 % does for Exceptions.
428 %    \begin{macrocode}
429 \pytex{p1 = pytex.PyTeX_Storage('problem1'); p1.load();
430        assert p.b > p1.b, ('b2 = '+str(p.b)+' > '+str(p1.b)+' = b1')}
431
432 \end{document}
433 %</example>
434 %    \end{macrocode}
435 %
436 %
437 % \Finale
438 \endinput