运行 PyRun_String() in Python C API 时如何用代码注释行?

How to annotate lines with code when running PyRun_String() in Python C API?

我正在使用 PyRun_String() 从 Python C API 到 运行 python 代码。

startglobalslocals传递Py_file_input我传递用PyDict_New()创建的字典,为代码字符串str 我通过我的代码。

例如我有下一个代码:

def f():
    def g():
        assert False, 'TestExc'
    g()
f()

当然,这段代码会抛出异常并显示堆栈。我使用 PyErr_Print() 打印错误并获取下一个堆栈:

Traceback (most recent call last):
  File "<string>", line 5, in <module>
  File "<string>", line 4, in f
  File "<string>", line 3, in g
AssertionError: TestExc

正如你所看到的,这个异常堆栈缺少代码行,例如,如果相同的脚本在纯 Python 解释器中是 运行,那么它会打印下一个堆栈:

Traceback (most recent call last):
  File "test.py", line 5, in <module>
    f()
  File "test.py", line 4, in f
    g()
  File "test.py", line 3, in g
    assert False, 'TestExc'
AssertionError: TestExc

所以它有代码注释,例如assert False, 'TestExc' 用于堆栈的最后一行,f()g() 用于前几行。它还有文件名(但文件名不是很重要)。

有什么方法可以在使用 PyRun_String() 时显示此代码?我希望我需要使用另一个函数,例如 PyRun_StringFlags(),它有额外的参数 PyCompilerFlags * flags,我可以用它来告诉编译器在编译时保存附加到每一行的代码。但是我没有在任何地方看到 PyCompilerFlags 的文档,也不知道我应该传递什么。

也许 PyCompilerFlags 还有其他有用的标志,例如在异常堆栈中使用 test.py 文件名而不是 <string> 会很好,可能这种行为也是可以调整的通过某些 PyCompilerFlags 值?

我还使用 Python 的内置函数 exec(),将带有程序代码的字符串传递给它。但是在没有代码注释的情况下得到了相同的异常堆栈。似乎如果它是一些解释器范围的参数是否保存代码注释。

我还尝试使用标准 tracebacksys 模块编写特殊函数来获取当前堆栈:

def GetStack():
    import traceback, sys
    frame = sys._getframe()
    extracted = traceback.extract_stack(frame)
    def AllFrames(f):
        while f is not None:
            yield f
            f = f.f_back
    all_frames = list(reversed(list(AllFrames(frame))))
    assert len(extracted) == len(all_frames)
    return [{
        'file': fs.filename, 'first_line': fr.f_code.co_firstlineno,
        'line': fs.lineno, 'func': fs.name, 'code': fs._line,
    } for fs, fr in zip(extracted, all_frames)]

此函数 returns 整个堆栈正确,但在 code 字段内有空字符串。看起来 frame 对象在其 ._line 属性中没有代码注释,因为它们可能应该如此,这可能是上面使用的所有函数中没有代码注释的原因。

你知道有没有办法为所有的栈retrieving/printing操作提供代码注解?除了手动编写正确的堆栈跟踪之外。也许至少有一些标准模块允许以某种方式至少手动设置这行代码?

更新。我发现 traceback 模块使用 linecache.getline(self.filename, self.lineno) (see here) 来获取源代码。有人知道如何在不使用临时文件的情况下使用给定文件名的内存中的源文本填充行缓存吗?

如果引发的异常使用 traceback 模块将异常输出到控制台或者它可能有自己的格式化实现也很有趣?

回答我自己的问题。在阅读 PyRun_String()source code 之后,我发现不可能注释 code-lines 异常(除非我遗漏了什么)。

因为 PyRun_String() 将文件名设置为 "<string>" 并且不允许提供其他名称,而异常打印代码尝试从文件系统读取文件,当然找不到这个文件名。

但我发现了如何使用 Py_CompileString() with PyEval_EvalCode() 来实现线注释,而不是仅使用 PyRun_String()。

基本上我在 tempfile 标准模块的帮助下创建临时文件。您也可以创建 non-temporary 文件,没关系。然后将源代码写入此文件并向 Py_CompileString() 提供文件名。在此之后,行被正确注释。

下面的代码是用 C++ 编写的,但您也可以在 C 中使用它,只需稍作调整(比如使用 PyObject * 而不是 auto)。

重要。为了我的代码简单起见,我不处理所有函数的错误,也不对对象进行引用计数。最后我也没有删除临时文件。这些所有的事情都应该在真实的程序中完成。因此,下面的代码不能直接用于生产,不加修改。

Try it online!

#include <Python.h>

int main() {
    Py_SetProgramName(L"prog.py");
    Py_Initialize();
    
    char const source[] = R"(
def f():
    def g():
        assert False, 'TestExc'
    g()
f()
)";
    
    auto pTempFile = PyImport_ImportModule("tempfile");
    
    auto pDeleteFalse = PyDict_New();
    PyDict_SetItemString(pDeleteFalse, "delete", Py_False);
    
    auto pFile = PyObject_Call(
        PyObject_GetAttrString(pTempFile, "NamedTemporaryFile"),
        PyTuple_Pack(0), pDeleteFalse);
    auto pFileName = PyObject_GetAttrString(pFile, "name");
    
    PyObject_CallMethod(pFile, "write", "y", source);
    PyObject_CallMethod(pFile, "close", nullptr);
    
    auto pCompiled = Py_CompileString(
        source, PyUnicode_AsUTF8(pFileName), Py_file_input);
    auto pGlobal = PyDict_New(), pLocal = PyDict_New();
    auto pEval = PyEval_EvalCode(pCompiled, pGlobal, pLocal);
    PyErr_Print();
    
    Py_FinalizeEx();
}

输出:

Traceback (most recent call last):
  File "C:\Users\User\AppData\Local\Temp\tmp_73evamv", line 6, in <module>
    f()
  File "C:\Users\User\AppData\Local\Temp\tmp_73evamv", line 5, in f
    g()
  File "C:\Users\User\AppData\Local\Temp\tmp_73evamv", line 4, in g
    assert False, 'TestExc'
AssertionError: TestExc