使用 Listings 包将 Python 代码与 LaTeX 文档同步

使用 Listings 包将 Python 代码与 LaTeX 文档同步

我有一个 Tex 文档,它使用 listings 包来打印 Python 源代码。我还想单独打印文档中的某些 Python 函数签名。我不想使用 listings 包中的行号功能,因为 Python 代码将来可能会被编辑。

有没有办法可以自动将 Python 代码的某些部分与 Tex 文档同步(不使用行号)?

非常感谢!

答案1

主要问题是你需要分析 Python 源代码,而 TeX 无法(轻易)做到这一点。幸运的是,Python 能够分析自身,使用抽象语法树(另见一些更详细的文档)。我们的想法是让 Python ast 分析代码并将其发现的内容写入 TeX 文件。

#!/usr/local/bin/python3

import ast

""" The python file we want to analyze.  Happens to be itself """
pythonfilename = 'pythonlinenumbers.py'

newcommands = []

def makenewcommand(command,
                   output):
    """ Turns the command and line number into the appropriate command.
    The signature is split onto two lines to make it more complicated.
    We have to play tricks with the trailing \, because we can't end a string
    with a single backslash. """
    return r'\newcommand{''\\'+command+'}{'+str(output)+'}\n'

class FuncLister(ast.NodeVisitor):
    def visit_FunctionDef(self, node):
        """ Recursively visit all functions, determining where each function
        starts, where its signature ends, and where the function ends.  Store
        these in the TeX variables \firstline@funcname, \sigend@funcname,
        \docend@funcname, and \lastline@funcname. """
        newname=node.name.replace('_','@') # _ isn't allowed in a TeX command
        newcommands.append(makenewcommand('firstline@'+newname,node.lineno))
        sigend = max(node.lineno,lastline(node.args))
        newcommands.append(makenewcommand('sigend@'+newname,sigend))
        docstring = ast.get_docstring(node)
        docstringlength = len(docstring.split('\n')) if docstring else -1
        newcommands.append(makenewcommand('docend@'+newname,sigend+docstringlength))
        newcommands.append(makenewcommand('lastline@'+newname,lastline(node)))
        self.generic_visit(node)

def lastline(node):
    """ Recursively find the last line of a node """
    return max( [ node.lineno if hasattr(node,'lineno') else -1 , ]
                +[lastline(child) for child in ast.iter_child_nodes(node)] )

with open(pythonfilename) as f:
    code = f.read()
FuncLister().visit(ast.parse(code))
with open('linenumbers.tex','w') as f:
    for newcommand in newcommands:
        f.write(newcommand)

创建文件linenumbers.tex

\newcommand{\firstline@makenewcommand}{10}
\newcommand{\sigend@makenewcommand}{11}
\newcommand{\docend@makenewcommand}{15}
\newcommand{\lastline@makenewcommand}{16}
\newcommand{\firstline@visit@FunctionDef}{19}
\newcommand{\sigend@visit@FunctionDef}{19}
\newcommand{\docend@visit@FunctionDef}{23}
\newcommand{\lastline@visit@FunctionDef}{32}
\newcommand{\firstline@lastline}{34}
\newcommand{\sigend@lastline}{34}
\newcommand{\docend@lastline}{35}
\newcommand{\lastline@lastline}{37}

然后,您可以让主 TeX 文件输入派生的 TeX 文件,并使用它找到的内容:

\documentclass{article}

\usepackage{listings}

\immediate\write18{python3 pythonlinenumbers.py}
\makeatletter
\input{linenumbers}
\newcommand{\showfunc}[1]{%
 #1 signature:
 \lstinputlisting[
    firstline=\csname firstline@#1\endcsname,
    lastline=\csname sigend@#1\endcsname,
    language=Python]
 {pythonlinenumbers.py}

 #1 in its entirety:
 \lstinputlisting[
    firstline=\csname firstline@#1\endcsname,
    lastline=\csname lastline@#1\endcsname,
    language=Python]
 {pythonlinenumbers.py}}
\makeatother

\begin{document}

Here's the lastline function:

\showfunc{lastline}

\bigskip

Here's the makenewcommand function (also has a multi-line signature):

\showfunc{makenewcommand}

\end{document}

这导致

pdf 输出

答案2

这是适用于类 unix 系统的另一种方法sed(已在 windows+cygwin 上测试)。您需要运行--enable-write18

\documentclass{article}
\usepackage{listings}

% Here we read the occurence of "def #2" and "end def #2" from 
% file #1 into lstart and lstop respectively  
% method taken from http://tex.stackexchange.com/a/70858/28808 thanks 
% to Antal Spector-Zabusky
\newread\myinput
\newcommand{\readfunc}[2]{% first parameter is the filename, second the function name
    \leavevmode
    \immediate\write18{sed -n '/def #2/=;//q' <#1 >\jobname_start.txt}
    \immediate\write18{sed -n '/end def #2/=;//q' <#1 >\jobname_stop.txt}
    \openin\myinput=\jobname_start.txt
    \bgroup
        \endlinechar=-1
        \read\myinput to \localline
        % Since everything in the group is local, we have to explicitly make the
        % assignment global
        \global\let\lstart\localline
    \egroup
    \openin\myinput=\jobname_stop.txt
    \bgroup
        \endlinechar=-1
        \read\myinput to \localline
        \global\let\lstop\localline
    \egroup
    \lstinputlisting[firstline=\lstart,lastline=\lstop]{#1}
}

这使用 GNU sed 来查找“def myfunc...”行,并依赖于匹配的“end def myfunc”注释。我的 python 脚本中有一些相当长的定义,所以无论如何都倾向于这样做。测试代码:

\begin{filecontents}{test.py}
#a comment
def myfunc():
    print "This is a function"
#end def myfunc

myfunc()
\end{filecontents}

\begin{document}
\subsection{Listing 1}
\readfunc{test.py}{myfunc}
\end{document}

临时文件用于保存 sed 的输出,不会被删除。输入代码可能可以根据我所写的内容进行优化。

相关内容