% File: pythonimmediate.sty % Copyright 2022-2024 user202729 % % This work may be distributed and/or modified under the conditions of the % LaTeX Project Public License (LPPL), either version 1.3c of this license or % (at your option) any later version. The latest version of this license is in % the file: % % http://www.latex-project.org/lppl.txt % % This work has the LPPL maintenance status `maintained'. % % The Current Maintainer of this work is user202729. \ProvidesExplPackage{pythonimmediate}{2024/01/18}{0.5.0}{Library to run Python code} \RequirePackage{saveenv} \PassOptionsToPackage{abspath}{currfile} % try to enable abspath if possible (i.e. if package isn't loaded) but otherwise don't give option-clash error \RequirePackage{currfile} \RequirePackage{l3keys2e} \RequirePackage{precattl} \cs_generate_variant:Nn \str_set:Nn {NV} \cs_generate_variant:Nn \str_if_eq:nnT {VnT} \cs_generate_variant:Nn \msg_error:nnn {nnV} \cs_generate_variant:Nn \str_if_eq:nnF {xnF} \cs_generate_variant:Nn \str_range:nnn {Vnn} \cs_generate_variant:Nn \str_if_eq:nnF {VnF} \cs_generate_variant:Nn \str_if_in:nnF {VnF} \cs_generate_variant:Nn \tl_build_put_right:Nn {NV} % some old commands e.g. \$, \^, \_, \~ require \set@display@protect to be robust. % ~ needs to be redefined directly. \precattl_exec:n { \cs_new_protected:Npn \_pythonimmediate_begingroup_setup_estr: { \begingroup \escapechar=-1~ \cC{set@display@protect} \let \cA\~ \relax } } % use like this \__begingroup_setup_estr: ⟨some command⟩ \endgroup % e.g. \__begingroup_setup_estr: \exp_args:NNx \endgroup \__function:n {⟨text⟩} % will eventually execute to \__function:n {⟨estr-expansion of text⟩} \str_gset:Nn \_pythonimmediate_args{} \str_gset:Nn \_pythonimmediate_python_executable{python3} \str_gset:Nn \_pythonimmediate_python_flags{} \keys_define:nn{pythonimmediate}{ args.tl_gset:N =\_pythonimmediate_args, python-executable.tl_gset:N=\_pythonimmediate_python_executable, python-flags.tl_gset:N =\_pythonimmediate_python_flags, child-process.bool_gset:N =\_pythonimmediate_child_process, } \ProcessKeysOptions{pythonimmediate} \_pythonimmediate_begingroup_setup_estr: \str_gset:Nx \_pythonimmediate_args { \_pythonimmediate_args } \str_gset:Nx \_pythonimmediate_python_executable{ \_pythonimmediate_python_executable } \str_gset:Nx \_pythonimmediate_python_flags { \_pythonimmediate_python_flags } \endgroup \msg_new:nnn {pythonimmediate} {shell-fail} {Please~enable~unrestricted~shell~escape!} \msg_new:nnn {pythonimmediate} {process-start-error} {Cannot~start~Python~process!~ Make~sure~package~options~are~correct~and~the~Python~package~is~installed.} \msg_new:nnn {pythonimmediate} {internal-error} {Internal~error!} \sys_if_shell_unrestricted:F { \msg_error:nn {pythonimmediate} {shell-fail} } % note on \newread: we need to persistently open the file anyway, so using LaTeX3 stream reference counting doesn't help % ======== setup read file ======== \bool_if:NTF \_pythonimmediate_child_process { % make read file read from stdin % note that \ior_str_get:NN cannot be used here as it checks for \ifeof which breaks with terminal for some reason \int_const:Nn \_pythonimmediate_read_file {-1} } { \newread \_pythonimmediate_read_file \openin \_pythonimmediate_read_file=|"\_pythonimmediate_python_executable \space \_pythonimmediate_python_flags \space -m ~ pythonimmediate.pytotex ~ \_pythonimmediate_args"~ } % ======== copy of ior_str_get but does not check for file end ======== \cs_generate_variant:Nn \use_ii_i:nn {o} \cs_new_protected:Npn \_pythonimmediate_str_get:N #1 { \use_ii_i:on {\the\endlinechar\relax} { \endlinechar=-1~ \readline \_pythonimmediate_read_file to #1 \endlinechar= } } % ======== setup write file ======== \cs_new_protected:Npn \_pythonimmediate_send_content:e #1 { \immediate\write \_pythonimmediate_write_file { #1 } } \cs_new_protected:Npn \_pythonimmediate_close_write: { \immediate\closeout \_pythonimmediate_write_file % safeguard, in some mode e.g. nonstopmode after a Python error TeX might continue \cs_gset_eq:Nc \_pythonimmediate_write_file {m@ne} \cs_gset_protected:Npn \_pythonimmediate_send_content:e ##1 {} } \bool_if:NTF \_pythonimmediate_child_process { \newwrite \_pythonimmediate_write_file \sys_if_engine_luatex:TF { % use Lua to write to stderr \directlua{ (require "pythonimmediate_helper")() } } { %\immediate\openout \_pythonimmediate_write_file=symlink-to-stderr.txt~ % tried this method, it seems to work except that it's buffered when stderr is redirected to a file... \immediate\openout \_pythonimmediate_write_file=|"\_pythonimmediate_python_executable \space \_pythonimmediate_python_flags \space -m ~ pythonimmediate.copy_to_stderr"~ } } { \sys_if_engine_luatex:TF { % use lua's io.popen % the other method works as well, but this one allows explicit flushing because on some TeX distribution it does not work \directlua{ (require "pythonimmediate_helper")( "\luaescapestring{ \_pythonimmediate_python_executable \space \_pythonimmediate_python_flags \space -m ~ pythonimmediate.textopy ~ \_pythonimmediate_args }" ) } } { \newwrite \_pythonimmediate_write_file \immediate\openout \_pythonimmediate_write_file=|"\_pythonimmediate_python_executable \space \_pythonimmediate_python_flags \space -m ~ pythonimmediate.textopy ~ \_pythonimmediate_args"~ % note that openout, even to shell, will append .tex if there's no dot in the command % so we artificially add a dot here in `.textopy` } % the command-line arguments passed to textopy above are only used initially before establishing a connection % after connection is made then the configuration object is sent over % both processes must be before the \readline below so that the 2 Python processes are started "in parallel" \_pythonimmediate_str_get:N \_pythonimmediate_line % read one line from pytotex half, forward to textopy half \str_if_eq:VnT \_pythonimmediate_line {} { \msg_error:nn {pythonimmediate} {process-start-error} } \str_const:Nn \_pythonimmediate_engine_mark_pdftex {p} \str_const:Nn \_pythonimmediate_engine_mark_ptex {P} \str_const:Nn \_pythonimmediate_engine_mark_uptex {u} \str_const:Nn \_pythonimmediate_engine_mark_xetex {x} \str_const:Nn \_pythonimmediate_engine_mark_luatex {l} \_pythonimmediate_send_content:e { \use:c {_pythonimmediate_engine_mark_ \c_sys_engine_str} \_pythonimmediate_line } } % ======== bootstrap utility functions % read one block of \TeX\ code from Python, store into the specified variable % the block is delimited using |surround_delimiter()| in Python i.e. the first and last line are identical % new lines are represented with ^^J \cs_new_protected:Npn \_pythonimmediate_gread_block:N #1 { \begingroup \endlinechar=10~ % affect \readline \readline \_pythonimmediate_read_file to \_pythonimmediate_delimiter \tl_build_gbegin:N #1 \readline \_pythonimmediate_read_file to \_pythonimmediate_line %\bench read~first~line. \bool_do_until:nn {\tl_if_eq_p:NN \_pythonimmediate_delimiter \_pythonimmediate_line} { \tl_build_gput_right:NV #1 \_pythonimmediate_line \readline \_pythonimmediate_read_file to \_pythonimmediate_line } \tl_build_gend:N #1 \endgroup } \cs_generate_variant:Nn \tl_build_gput_right:Nn {NV} \cs_new_protected:Npn \_pythonimmediate_read_block:N #1 { \_pythonimmediate_gread_block:N \_pythonimmediate_block \tl_set_eq:NN #1 \_pythonimmediate_block } % read one block of \TeX\ code from Python and |\scantokens|-run it % the content inside is the actual TeX code to be executed \cs_new_protected:Npn \_pythonimmediate_run_block: { \_pythonimmediate_gread_block:N \_pythonimmediate_code \begingroup \newlinechar=10~ \expandafter \endgroup \scantokens \expandafter{\_pythonimmediate_code} } % trick described in https://tex.stackexchange.com/q/640274 to scantokens the code with \newlinechar=10 % ======== bootstrap code \_pythonimmediate_run_block: