summaryrefslogtreecommitdiff
path: root/macros/latex/contrib/pythonimmediate/pythonimmediate.tex
diff options
context:
space:
mode:
Diffstat (limited to 'macros/latex/contrib/pythonimmediate/pythonimmediate.tex')
-rw-r--r--macros/latex/contrib/pythonimmediate/pythonimmediate.tex535
1 files changed, 535 insertions, 0 deletions
diff --git a/macros/latex/contrib/pythonimmediate/pythonimmediate.tex b/macros/latex/contrib/pythonimmediate/pythonimmediate.tex
new file mode 100644
index 0000000000..877bc3a05e
--- /dev/null
+++ b/macros/latex/contrib/pythonimmediate/pythonimmediate.tex
@@ -0,0 +1,535 @@
+\ProvidesFile{pythonimmediate.tex}[2022/12/23 0.0.0 Library to run Python code]
+\RequirePackage{fvextra}
+\documentclass{l3doc}
+\usepackage{tikz}
+\usetikzlibrary{calc}
+\usetikzlibrary{arrows.meta}
+\EnableCrossrefs
+\CodelineIndex
+\fvset{breaklines=true,gobble=0,tabsize=4,frame=single,numbers=left,numbersep=3pt}
+
+\AtBeginDocument{\DeleteShortVerb\"} % https://tex.stackexchange.com/a/650966/250119
+\MakeOuterQuote{"}
+
+\newcommand\reg{régime}
+\newcommand\DescribePython{\DescribeMacro} % hack
+
+
+\begin{document}
+
+
+% hack to make quotes inside |...| straight
+\edef\temp{\def\noexpand|{\noexpand\Verb\string|}}\temp
+% basically execute \def| { \Verb ⟨catcode other |⟩ }
+
+\GetFileInfo{\jobname.tex}
+
+\title{\pkg{\jobname} --- \fileinfo
+\thanks{This file describes version \fileversion, last revised \filedate.}
+}
+\author{user202729}
+\date{Released \filedate}
+
+\maketitle
+
+\begin{abstract}
+ \fileinfo.
+\end{abstract}
+
+\section{Motivation}
+
+Just like \pkg{Perl\TeX} or \pkg{PyLua\TeX} (and unlike \pkg{Python\TeX} or \pkg{lt3luabridge}),
+this only requires a single run, and variables are persistent throughout the run.
+
+Unlike \pkg{Perl\TeX} or \pkg{PyLua\TeX}, there's no restriction on compiler or script required to run the code.
+
+There's also debugging functionalities -- \TeX\ errors results in Python traceback, and Python error results in \TeX\ traceback.
+Errors in code executed with the |pycode| environment gives the correct traceback point to the Python line of code in the \TeX\ file.
+
+For advanced users, this package allows the user to manipulate the \TeX\ state directly from within Python,
+so you don't need to write a single line of \TeX\ code.
+
+\section{Installation}
+
+%You need to install Python \pkg{daemoniker} and \pkg{psutil} package.
+
+The package should work out of the box with no additional Python package needed.
+
+\subsection{Installation on Overleaf}
+
+At the point of writing, this package can be used on Overleaf.
+
+Instruction:
+
+\begin{itemize}
+ \item Download the following files and place it in the root folder of Overleaf:
+ \begin{itemize}
+ \item \file{saveenv.sty}
+ \item \file{precattl.sty}
+ \item \file{pythonimmediate.sty}
+ \item \file{pythonimmediate_script_textopy.py}
+ \item \file{pythonimmediate_script_pytotex.py}
+ \end{itemize}
+ \item Write the following in the preamble:
+\begin{verbatim}
+\usepackage[abspath]{currfile}
+\usepackage[mode=unnamed-pipe]{pythonimmediate}
+\end{verbatim}
+Refer to \ref{troubleshoot-source-file-not-found} for explanation of the |abspath| option.
+\end{itemize}
+
+For some unknown reason in the default mode on Overleaf (|\nonstopmode|), when there's an error
+the log file might be truncated,
+so in that case consider writing |\errorstopmode|.
+
+Refer to \ref{troubleshoot-Python-error} to read the error traceback in case of Python error.
+
+\section{Usage}
+
+\subsection{Package options}
+
+\DescribeOption{outputdir=}
+Specify the output directory if it's not the default value.
+
+\begin{texnote}
+ The value will be |x|-expanded.
+\end{texnote}
+
+Using the \pkg{outputdir} package, it's possible to determine the output directory automatically, subject to restrictions.
+
+An usage example, if you invoked |pdflatex| with the command
+\begin{verbatim}
+pdflatex --output-directory=/tmp/ a.tex
+\end{verbatim}
+then in the file \file{a.tex} you should have
+\begin{verbatim}
+\usepackage[outputdir=/tmp/]{pythonimmediate}
+\end{verbatim}
+
+\DescribeOption{mode=}
+Method to communicate between \TeX\ and Python. Usually the method will be automatically detected.
+
+Possible values include |multiprocessing-network| and |unnamed-pipe|.
+
+\subsection{\TeX\ interface}
+
+The interface mimics those in popular packages such as \pkg{Python\TeX} or \pkg{PyLua\TeX}.
+
+\subsubsection{Inline commands}
+
+\DescribeMacro{\py}\label{py-command}
+Evaluate some Python expression, consider the result as a string, then execute the result as \TeX\ command.
+
+\begin{texnote}
+ The command is not expandable, and the argument will be fully expanded with the active |~| set to |\relax|,
+ |\set@display@protect| executed and |\escapechar=-1|, then the result
+ passed to Python as a string.
+\end{texnote}
+
+Which, for the users who are not familiar with \TeX\ terminology, roughly means the following:
+\begin{itemize}
+ \item the value can only be used to typeset text,
+ it must not be used to pass "values" to other \LaTeX\ commands.
+
+ The following is legal:
+\begin{verbatim}
+The value of $1+1$ is $\py{1+1}$.
+\end{verbatim}
+The following is illegal, as the result (2) can only be used to typeset text, not passed to another command that expect a "value":
+\begin{verbatim}
+\setcounter{abc}{\py{1+1}}
+\end{verbatim}
+A possible workaround is:
+\begin{verbatim}
+\py{ r'\\setcounter{abc}{' + str(1+1) + '}' }
+\end{verbatim}
+In this example it works without escaping the |{}| characters, but if the Python code has those unbalanced then you can escape them as mentioned below.
+
+ \item Special characters can be "escaped" simply by prefixing the character with backslash.
+
+ For example
+\begin{verbatim}
+\pyc{assert len('\ \ ')==2}
+\pyc{assert ord('\\\\')==0x5c}
+\pyc{assert ord('\%') ==0x25}
+\end{verbatim}
+In the examples above, Python "sees" (i.e. the Python code being executed is)
+\begin{verbatim}
+assert len(' ')==2
+assert ord('\\')==0x5c
+assert ord('%') ==0x25
+\end{verbatim}
+respectively.
+
+ \item Macros will be expanded.
+\begin{verbatim}
+\def\mycode{1+1}
+The value of $1+1$ is $\py{\mycode}$.
+\end{verbatim}
+\end{itemize}
+
+
+\DescribeMacro{\pyc}
+Execute some Python code provided as an argument (the argument will be interpreted as described above).
+
+The command is not expandable -- roughly speaking, you can only use this at "top level".
+
+Any output (as described in \ref{print-to-TeX}) will be typesetted.
+
+The difference between |\py| and |\pyc| is that the argument of |\py| should be a Python expression
+(suitable for passing into |eval()| Python function) while the argument of |\pyc| should be a Python
+statement (suitable for passing into |exec()| Python function).
+
+Therefore,
+\begin{itemize}
+ \item |\py{1+1}| will typeset 2.
+ \item |\pyc{1+1}| is valid, but will do nothing just like |exec("1+1")|.
+ \item |\py{x=1}| is invalid.
+ \item |\pyc{x=1}| is valid and assigns the variable |x| to be 1.
+\end{itemize}
+
+\DescribeMacro{\pycq}
+Same as above, but output (\ref{print-to-TeX}) will not be typesetted.
+
+\DescribeMacro{\pyfile}
+Given an argument being the file name, execute that file.
+
+\DescribeMacro{\pys}
+Performs "string interpolation", the same way as \pkg{Python\TeX}.
+(not yet implemented)
+
+\subsubsection{Environments}
+
+\DescribeEnv{pycode}
+Verbatim-like environment that executes the code inside as Python.
+
+Example usage: The following will typeset |123|
+\begin{verbatim}
+\begin{pycode}
+pythonimmediate.print("123")
+\end{pycode}
+\end{verbatim}
+
+Special note: white spaces at the end of lines are preserved.
+
+Any output (as described in \ref{print-to-TeX}) will be typesetted.
+
+\DescribeEnv{pycodeq}
+Same as above, but output will not be typesetted.
+
+\DescribeEnv{pysub}
+Not yet implemented.
+
+\subsection{Python interface}
+
+The \TeX\ interface is only used to call Python. Apart from that, all the work can be done on the Python side.
+
+All functions in this section should be imported from |pythonimmediate| package, unless specified otherwise.
+
+\subsubsection{Print to \TeX}\label{print-to-TeX}
+
+\DescribePython{.print()}
+\DescribePython{.file}
+These functions are used in |\pyc| command or |pycode| environment.
+
+Unlike most other packages, using |print()| function in Python will print to the console (\TeX\ standard output).
+In order to print \TeX\ code to be executed, you can do one of
+\begin{verbatim}
+pythonimmediate.print(...)
+print(..., file=pythonimmediate.file)
+with contextlib.redirect_stdout(pythonimmediate.file):
+ print(...)
+\end{verbatim}
+Note that in quiet environments, |pythonimmediate.file| is None, the second variant using |print()| will print to stdout
+instead of suppress output. The third variant works as expected.
+
+All output will be buffered until the whole Python code finishes executing.
+In order to typeset the text immediately use one of the advanced commands.
+
+
+
+\DescribePython{.newcommand()}
+\DescribePython{.renewcommand()}
+Same as \LaTeX's |\newcommand| and |\renewcommand|. Can be used as follows:
+
+\begin{verbatim}
+from pythonimmediate import newcommand, renewcommand
+
+@newcommand
+def function():
+ ...
+# define |\function| in TeX
+
+@newcommand("controlsequencename")
+def function():
+ ...
+# define |\controlsequencename| in TeX
+
+def function():
+ ...
+newcommand("controlsequencename", function)
+\end{verbatim}
+
+\DescribePython{.get_arg_str()}
+\DescribePython{.get_optional_arg_str()}
+\DescribePython{.get_verb_arg()}
+\DescribePython{.get_multiline_verb_arg()}
+\DescribePython{.peek_next_char()}
+\DescribePython{.get_next_char()}
+There are those functions that is mostly understandable to an inexperienced \LaTeX\ user,
+and should be sufficient for a lot of programming works.
+
+This is an example of how the functions could be used. The name should be mostly self-explanatory.
+
+\begin{verbatim}
+\documentclass{article}
+\usepackage{pythonimmediate}
+\begin{document}
+\begin{pycode}
+from pythonimmediate import newcommand, peek_next_char, get_next_char, get_arg_str, print
+@newcommand
+def innerproduct():
+ s = get_arg_str() # in the example below this will have the value '\mathbf{a},\mathbf{b}'
+ x, y = s.split(",") # it's just a Python string, manipulate normally (but be careful of comma inside braces, parse the string yourself)
+ print(r"\left\langle" + x + r"\middle|" + y + r"\right\rangle")
+
+@newcommand
+def fx():
+ if peek_next_char() == "_":
+ get_next_char()
+ subscript = get_arg_str()
+ print("f_{" + subscript + "}(x)")
+ else:
+ print("f(x)")
+
+@newcommand
+def sumManyArgs():
+ s = 0
+ while peek_next_char() == "{":
+ i = get_arg_str()
+ s += int(i)
+ print(str(s))
+\end{pycode}
+$1+2+3 = \sumManyArgs{1}{2}{3}$
+
+$\innerproduct{\mathbf{a},\mathbf{b}}=1$
+
+$\fx = 1$, $\fx_i = 2$, $\fx_{ij} = 3$
+\end{document}
+\end{verbatim}
+
+It will typeset:
+
+\begin{quote}
+$1+2+3=6$
+
+$\left\langle\mathbf{a}\middle\vert\mathbf{b}\right\rangle=1$
+
+$f(x)=1$, $f_i(x)=2$, $f_{ij}(x)=3$
+\end{quote}
+
+\DescribePython{.get_arg_estr()}
+\DescribePython{.get_optional_arg_estr()}
+Similar to some functions above, except that the argument is fully expanded and "escapes" of common characters are handled correctly,
+similar to how |\py| command (\ref{py-command}) reads its arguments.
+
+\DescribePython{.execute()}
+Takes a string and execute it immediately. (so that any |.execute()| will be executed before any |.print()|)
+
+Assuming \TeX\ is in errorstopmode (i.e. errors halt \TeX\ execution),
+any error in \TeX\ will create an error in Python and the traceback should point to the correct line of code.
+
+For example, in the following code
+
+\begin{verbatim}
+\documentclass{article}
+\usepackage{tikz}
+\usepackage{pythonimmediate}
+\begin{document}
+
+\begin{tikzpicture}
+\begin{pycode}
+from pythonimmediate import execute
+execute(r'\draw (0, 0) to (1, 1);')
+execute(r'\draw (2, 2) to (p);')
+execute(r'\draw (3, 3) to (4, 4);')
+\end{pycode}
+\end{tikzpicture}
+
+\end{document}
+\end{verbatim}
+each |\draw| command will be executed immediately when the Python |.execute()| function is executed,
+and as the second line throws an error, the Python traceback will point to that line.
+
+\section{Troubleshooting}
+
+\subsection{"Source file not found!" error message}\label{troubleshoot-source-file-not-found}
+
+In order to obtain the exact code with trailing spaces and produce error traceback
+point to the correct \TeX\ file, the Python code need to know the full path to the current
+\TeX\ file for the |pycode| environment.
+
+Nevertheless, this is difficult and does not always work
+(refer to the documentation of \pkg{currfile} for details), so this message is issued
+when the file cannot be found.
+
+In that case try the following fixes:
+
+\begin{itemize}
+ \item Include |\usepackage[abspath]{currfile}| at the start of the document, after the |\documentclass| line.
+ (this option is not included by default because it's easy to get package clash, and usually \pkg{currfile} without
+ the |abspath| option works fine -- unless custom |jobname| is used)
+ \item Explicitly override |currfilename| or |currfileabspath| -- for example
+\begin{verbatim}
+\def\currfilename{main.tex}
+\end{verbatim}
+ Technically this is an abuse of the \pkg{currfile} package API, but it usually works regardless.
+\end{itemize}
+
+\subsection{"Python error" error message}\label{troubleshoot-Python-error}
+
+In case of Python error, the Python traceback is included in the terminal and \TeX\ log file.
+
+Search for "Python error traceback" before the error line in the log file.
+
+On Overleaf, you can either view the log file ("Raw logs" section)
+or the traceback on stderr (download \file{output.stderr} file)
+
+\subsection{"\TeX\ error" error message}\label{troubleshoot-TeX-error}
+
+If an error occur in \TeX, traceback cannot be included in the log file.
+
+Besides, this can only be detected in |\errorstopmode|. Such an error will always halt \TeX,
+and Python will be force-exited after printing the error traceback.
+
+On Overleaf, download \file{output.stderr} file to read the traceback.
+
+\section{Implementation note}
+
+Communication between \TeX\ and Python are done by opening two pseudo-files from the output of a Python process |textopy|
+(similar to |\ior_shell_open:Nn|)
+and to the input of another Python process |pytotex| (this would be |\iow_shell_open:Nn|, if \LaTeX3 have such a function).
+
+There are various methods for the 2 Python child processes to communicate with each other.
+After some initial bootstrapping to setup the communication, we can consider only the |textopy| script, the other
+merely serves as the bridge to send input to \TeX.
+
+The communication protocol is a little complicated, since it must support nesting bidirectional execution of \TeX\ and Python.
+
+Besides, I believe it's not possible to make a "background listener" on the \TeX\ side, so it must keep track of whether a command should be read from Python and executed.
+
+Currently, exception handling (throwing a Python exception in a nested Python function, catch it in the outer Python function) is not supported.
+
+These are some examples of what could happen in the communication protocol.
+
+\ExplSyntaxOn
+\let\fpEval\fp_eval:n
+\ExplSyntaxOff
+
+\begingroup
+
+\tikzset{myarrow/.style={-{Latex[length=2mm]}}}
+
+\def\blockWidth{1}
+\def\blockHeight{7}
+\def\separation{10.5}
+\def\step{0.7}
+
+\def\left #1{
+ \draw [myarrow] (p) ++(\separation, 0) -- node [above] {\small #1} ++(-\separation, 0);
+ \path (p) ++ (0, -\step) coordinate (p);
+}
+\def\right #1{
+ \draw [myarrow] (p) -- node [above] {\small #1} ++(\separation, 0);
+ \path (p) ++ (0, -\step) coordinate (p);
+}
+
+\def\initDrawing{
+ \path (\blockWidth, \fpEval{-\step}) coordinate (p);
+}
+
+\newdimen \tempx
+\newdimen \tempy
+
+\ExplSyntaxOn
+\let \dimToDecimalInUnit \dim_to_decimal_in_unit:nn
+\ExplSyntaxOff
+
+\def\drawBlocks{
+ \path [transform canvas] (p);
+ \pgfgetlastxy{\tempx}{\tempy}
+ \edef\blockHeight{ \fpEval{ - \dimToDecimalInUnit{\tempy}{1cm} } }
+
+ \draw (0, \fpEval{-\blockHeight}) rectangle node [rotate=90] {\TeX} (\blockWidth, 0);
+ \draw (\fpEval{\blockWidth+\separation}, \fpEval{-\blockHeight}) rectangle node [rotate=270] {Python} (\fpEval{\separation+2*\blockWidth}, 0);
+}
+
+\vspace{5pt}
+\begin{tikzpicture}
+\edef\blockHeight{\fpEval{\step*3}}
+\initDrawing
+\right{execute Python code: print(1)}
+\left{execute \TeX\ code: 1}
+\drawBlocks
+\end{tikzpicture}
+
+Nevertheless, there may be more complicated cases where the Python code itself may call \TeX\ code before actually returns:
+
+\vspace{5pt}
+\begin{tikzpicture}
+\edef\blockHeight{\fpEval{\step*5}}
+\initDrawing
+\right{execute Python code: print(var(a)*2)}
+\left{execute \TeX\ code: sendtopy(a); execute another command}
+\right{123}
+\left{execute \TeX\ code: 123123}
+\drawBlocks
+\end{tikzpicture}
+
+Or:
+
+\vspace{5pt}
+\begin{tikzpicture}
+\edef\blockHeight{\fpEval{\step*7}}
+\initDrawing
+\right{execute Python code: tex.exec(a=456); print(var(a)*2)}
+\left{execute \TeX\ code: a=456; sendtopy(done); execute another command}
+\right{done}
+\left{execute \TeX\ code: sendtopy(a); execute another command}
+\right{456}
+\left{456456}
+\drawBlocks
+\end{tikzpicture}
+
+\endgroup
+
+The Python side must not just listen for "done" command back, but must potentially call a nested loop.
+
+The exact protocol is:
+\begin{itemize}
+ \item "execute Python code" sends from \TeX\ to Python has a single line "|i|\meta{handler name}",
+ followed by any number of arguments (depends on the handler).
+
+ Refer to the |define_TeX_call_Python| internal function for details.
+
+ \item "done" sends from \TeX\ to Python has the format "|r|\meta{optional return value as a string in a single line}".
+
+ This is sent by executing \TeX\ command |\pythonimmediatecontinue|, which takes a single argument to be e-expanded using |\write|
+ as the "return value".
+
+ \item "execute \TeX\ code" sends from Python to \TeX\ must only be sent when the \TeX\ side listens for a command.
+ It consist of a single line specify the "command name", which \TeX\ will
+ execute the command named
+ |\__run_|\meta{command name}|:|
+ which must already be defined on the \TeX\ side.
+
+ The command itself might contain additional code to execute more code, e.g. by reading more lines from Python.
+
+ Refer to the |define_Python_call_TeX| internal function for details.
+\end{itemize}
+
+
+
+\PrintIndex
+
+\end{document}