跳转到主要内容

从Python堆栈帧和回溯中提取数据以进行信息显示

项目描述

stack_data

Tests Coverage Status Supports Python versions 3.5+

这是一个从堆栈帧和回溯中提取数据的库,尤其是显示比默认更有用的回溯。它为IPython和futurecoder中的回溯提供支持。

futurecoder example

您可以从PyPI安装它

pip install stack_data

基本用法

这是我们想要检查的一些代码

def foo():
    result = []
    for i in range(5):
        row = []
        result.append(row)
        print_stack()
        for j in range(5):
            row.append(i * j)
    return result

请注意,foo调用了一个函数print_stack()。在现实中,我们可以想象在这一点上抛出了一个异常,或者调试器在这里停止了,但这很容易直接操作。以下是一个基本实现

import inspect
import stack_data


def print_stack():
    frame = inspect.currentframe().f_back
    frame_info = stack_data.FrameInfo(frame)
    print(f"{frame_info.code.co_name} at line {frame_info.lineno}")
    print("-----------")
    for line in frame_info.lines:
        print(f"{'-->' if line.is_current else '   '} {line.lineno:4} | {line.render()}")

(请注意,这里有一个严重的错误 - 它没有考虑行差距,我们将在后面了解这一点)

print_stack()单次调用的输出如下

foo at line 9
-----------
       6 | for i in range(5):
       7 |     row = []
       8 |     result.append(row)
-->    9 |     print_stack()
      10 |     for j in range(5):

《print_stack()`》的代码相当直观易懂。如果您想了解更多关于特定类或方法的信息,建议您查看一些docstrings。`FrameInfo`是一个类,它接受一个帧或traceback对象,并提供了一些有用的属性和属性(这些属性被缓存,因此您无需担心性能)。特别是,`frame_info.lines`是一个`Line`对象的列表。`line.render()`返回该行的源代码,适用于显示。如果没有参数,它只会去除任何公共的前导缩进。稍后我们还会看到它的更强大用途。

您可以看到`frame_info.lines`包括一些上下文行。默认情况下,它包括主行之前3个上下文片段和之后1个片段。我们可以通过传递选项来配置上下文数量。

options = stack_data.Options(before=1, after=0)
frame_info = stack_data.FrameInfo(frame, options)

然后输出看起来像这样:

foo at line 9
-----------
       8 | result.append(row)
-->    9 | print_stack()

请注意,这些参数不是包含在前后行数,而是包含的片段数。一个片段是一个文件中一个或多个逻辑上应组合在一起的行范围。一个片段包含一个单独的简单语句或复合语句(循环、if、try/except等)的一部分,该部分不包含任何其他语句。大多数片段是单行,但多行语句或`if`条件是一个片段。在上面的例子中,所有片段都是单行的,因为没有内容跨越多行。如果我们更改我们的代码以包含一些多行部分

def foo():
    result = []
    for i in range(5):
        row = []
        result.append(
            row
        )
        print_stack()
        for j in range(
                5
        ):
            row.append(i * j)
    return result

然后使用默认选项运行原始代码,那么输出是:

foo at line 11
-----------
       6 | for i in range(5):
       7 |     row = []
       8 |     result.append(
       9 |         row
      10 |     )
-->   11 |     print_stack()
      12 |     for j in range(
      13 |             5
      14 |     ):

现在第8-10行和第12-14行各是一个片段。请注意,从本质上讲,输出与原始代码的代码量相同。将文件划分为片段意味着上下文的边界直观,不会裁剪语句或表达式的部分。例如,如果上下文按行而不是按片段来衡量,上述的最后一条将是`for j in range(`,这不太有用。

然而,如果一个片段非常长,包括所有内容可能会很繁琐。为此,`Options`有一个参数`max_lines_per_piece`,默认值为6。假设我们的代码中有一个比这还长的片段

        row = [
            1,
            2,
            3,
            4,
            5,
        ]

`frame_info.lines`将截断这个片段,这样它将产生7个`Line`对象而不是,5个`Line`对象和一个`LINE_GAP`在中间,总共6个对象。我们的代码目前无法处理间隙,所以它会引发异常。我们可以这样修改它:

    for line in frame_info.lines:
        if line is stack_data.LINE_GAP:
            print("       (...)")
        else:
            print(f"{'-->' if line.is_current else '   '} {line.lineno:4} | {line.render()}")

现在输出看起来像这样:

foo at line 15
-----------
       6 | for i in range(5):
       7 |     row = [
       8 |         1,
       9 |         2,
       (...)
      12 |         5,
      13 |     ]
      14 |     result.append(row)
-->   15 |     print_stack()
      16 |     for j in range(5):

或者,您可以反转条件并检查`if isinstance(line, stack_data.Line):`。无论如何,您都应该始终检查行间隙,否则您的代码可能一开始看起来工作正常,但遇到长片段时可能会失败。

请注意,正在执行的片段,即包含当前正在执行的行的片段(在这个例子中是第15行),无论多长都不会被截断。

上下文字行永远不会超出`frame_info.scope`的范围,这是包含当前行的最内层函数或类定义。例如,这是一个没有在当前行之前和之后3行或1行的短函数的输出

bar at line 6
-----------
       4 | def bar():
       5 |     foo()
-->    6 |     print_stack()

有时确保始终显示函数签名很有用。这可以通过`Options(include_signature=True)`来完成。结果看起来像这样

foo at line 14
-----------
       9 | def foo():
       (...)
      11 |     for i in range(5):
      12 |         row = []
      13 |         result.append(row)
-->   14 |         print_stack()
      15 |         for j in range(5):

为了节省空间,片段永远不会以空白行开始或结束,并且片段之间的空白行被排除。所以如果我们的代码看起来像这样

    for i in range(5):
        row = []

        result.append(row)
        print_stack()

        for j in range(5):

输出变化不大,除了您可以看到行号跳跃

      11 |     for i in range(5):
      12 |         row = []
      14 |         result.append(row)
-->   15 |         print_stack()
      17 |         for j in range(5):

变量

您还可以检查帧中的变量和其他表达式,例如

    for var in frame_info.variables:
        print(f"{var.name} = {repr(var.value)}")

可能输出

result = [[0, 0, 0, 0, 0], [0, 1, 2, 3, 4], [0, 2, 4, 6, 8], [0, 3, 6, 9, 12], []]
i = 4
row = []
j = 4

`frame_info.variables`返回一个`Variable`对象的列表,它具有`name`、`value`和`nodes`属性,其中`nodes`是该表达式的所有AST表示的列表。

“变量”可能指代一个除了简单变量名之外的表达式。它可以是由库pure_eval评估的任何表达式,该库认为它是有趣的(更多信息请参阅相关文档)。这包括像foo.barfoo[bar]这样的表达式。在这些情况下,name是该表达式的源代码。《pure_eval》确保它只评估不会产生任何副作用的表达式,例如,当foo.bar是一个正常的属性而不是一个描述符(如属性)时。

frame_info.variablesframe_info.scope中找到的所有有趣表达式的列表,例如当前函数,这可能包括在frame_info.lines中不可见的表达式。您可以通过使用frame_info.variables_in_lines或甚至frame_info.variables_in_executing_piece来限制列表。为了获得更多控制,您可以使用frame_info.variables_by_lineno。有关更多信息,请参阅文档字符串。

使用范围和标记渲染行

有时,您可能希望为了显示目的在文本中插入特殊字符,例如HTML或ANSI颜色代码。stack_data提供了一些工具,使这变得更加容易。

假设我们有一个Line对象,其中line.text(该行的原始源代码)是"foo = bar",因此line.text[6:9]"bar",我们想通过在文本的6和9位置插入HTML来强调这部分。下面是如何直接做到这一点的方法

markers = [
    stack_data.MarkerInLine(position=6, is_start=True, string="<b>"),
    stack_data.MarkerInLine(position=9, is_start=False, string="</b>"),
]
line.render(markers)  # returns "foo = <b>bar</b>"

在这里,is_start=True表示标记是一对中的第一个。这有助于line.render()正确排序和插入标记,这样您就不会得到如foo<b>.<i></b>bar</i>这样的格式错误的HTML,其中标签重叠。

由于我们在插入HTML,我们应该使用line.render(markers, escape_html=True),这将转义Python源代码中的特殊HTML字符(但不包括标记),例如,foo = bar < spam将被渲染为foo = <b>bar</b> &lt; spam

通常,您不会直接创建标记。相反,您可以从一个或多个范围开始,然后将其转换为如下所示

ranges = [
    stack_data.RangeInLine(start=0, end=3, data="foo"),
    stack_data.RangeInLine(start=6, end=9, data="bar"),
]

def convert_ranges(r):
    if r.data == "bar":
        return "<b>", "</b>"        

# This results in `markers` being the same as in the above example.
markers = stack_data.markers_from_ranges(ranges, convert_ranges)

RangeInLine有一个data属性,可以是任何对象。markers_from_ranges接受一个转换函数,该函数将所有RangeInLine对象传递给它。如果转换函数返回一个字符串对,它将从中创建两个标记。否则,它应该返回None来指示该范围应被忽略,就像在这个例子中第一个范围包含"foo"一样。

这样做是有用的,因为有一些内置工具可以为您创建这些范围。例如,如果我们将我们的print_stack()函数更改为包含以下内容

def convert_variable_ranges(r):
    variable, _node = r.data
    return f'<span data-value="{repr(variable.value)}">', '</span>'

markers = stack_data.markers_from_ranges(line.variable_ranges, convert_variable_ranges)
print(f"{'-->' if line.is_current else '   '} {line.lineno:4} | {line.render(markers, escape_html=True)}")

那么输出将变成

foo at line 15
-----------
       9 | def foo():
       (...)
      11 |     for <span data-value="4">i</span> in range(5):
      12 |         <span data-value="[]">row</span> = []
      14 |         <span data-value="[[0, 0, 0, 0, 0], [0, 1, 2, 3, 4], [0, 2, 4, 6, 8], [0, 3, 6, 9, 12], []]">result</span>.append(<span data-value="[]">row</span>)
-->   15 |         print_stack()
      17 |         for <span data-value="4">j</span> in range(5):

line.variable_ranges是每个至少部分出现在该行中的变量的RangeInLines列表。范围的data属性是一对(variable, node),其中node是与该范围相对应的特定AST节点,该节点来自列表variable.nodes

您还可以使用line.token_ranges(例如,如果您想进行自己的语法高亮)或line.executing_node_ranges(如果您想突出显示由executing库标识的当前正在执行的节点)。或者,如果您想从AST节点创建自己的范围,请使用line.range_from_node(node, data)。有关更多信息,请参阅文档字符串。

使用Pygments进行语法高亮

如果您想轻松地获得美观的彩色文本,可以由Pygments为您完成。只需遵循以下步骤

  1. 单独作为它不是stack_data的依赖项,请使用pip install pygments
  2. 创建一个pygments格式化器对象,如HtmlFormatterTerminal256Formatter
  3. 将格式化器传递到Optionspygments_formatter参数中。
  4. 使用line.render(pygmented=True)来获取格式化的文本。在这种情况下,您不能向render传递任何标记。

如果您愿意,还可以结合pygments语法高亮,在框架中突出显示正在执行的节点。为此,您需要

  1. 一个pygments样式 - 要么是一个样式类,要么是一个命名它的字符串。请参阅样式文档样式画廊
  2. 对执行节点样式进行的修改,例如字符串"bold""bg:#ffff00"(黄色背景)。请参阅样式规则文档
  3. 将这些内容传递给stack_data.style_with_executing_node(style, modifier)以获取一个新的样式类。
  4. 在创建格式化程序时传递新样式。

注意,这在与TerminalFormatter不兼容,因为它仅使用基本的ANSI颜色,并且通常不使用传递给它的样式。

获取完整的堆栈

目前print_stack()实际上并没有打印堆栈,它只打印了一个框架。与其使用frame_info = FrameInfo(frame, options),不如这样做

for frame_info in FrameInfo.stack_data(frame, options):

现在输出看起来像这样

<module> at line 18
-----------
      14 |         for j in range(5):
      15 |             row.append(i * j)
      16 |     return result
-->   18 | bar()

bar at line 5
-----------
       4 | def bar():
-->    5 |     foo()

foo at line 13
-----------
      10 | for i in range(5):
      11 |     row = []
      12 |     result.append(row)
-->   13 |     print_stack()
      14 |     for j in range(5):

然而,正如frame_info.lines并不总是产生Line对象一样,FrameInfo.stack_data也不总是产生FrameInfo对象,我们必须修改我们的代码来处理这种情况。让我们看看一些不同的示例代码

def factorial(x):
    return x * factorial(x - 1)


try:
    print(factorial(5))
except:
    print_stack()

在这段代码中,我们忘记在factorial函数中包含一个基本情况,因此它将因RecursionError而失败,并将有多个具有类似信息的框架。与内置的Python跟踪记录类似,stack_data避免显示所有这些框架。相反,您将获得一个RepeatedFrames对象,该对象总结了信息。请参阅其文档字符串以获取更多详细信息。

这是我们的更新实现

def print_stack():
    for frame_info in FrameInfo.stack_data(sys.exc_info()[2]):
        if isinstance(frame_info, FrameInfo):
            print(f"{frame_info.code.co_name} at line {frame_info.lineno}")
            print("-----------")
            for line in frame_info.lines:
                print(f"{'-->' if line.is_current else '   '} {line.lineno:4} | {line.render()}")

            for var in frame_info.variables:
                print(f"{var.name} = {repr(var.value)}")

            print()
        else:
            print(f"... {frame_info.description} ...\n")

输出结果

<module> at line 9
-----------
       4 | def factorial(x):
       5 |     return x * factorial(x - 1)
       8 | try:
-->    9 |     print(factorial(5))
      10 | except:

factorial at line 5
-----------
       4 | def factorial(x):
-->    5 |     return x * factorial(x - 1)
x = 5

factorial at line 5
-----------
       4 | def factorial(x):
-->    5 |     return x * factorial(x - 1)
x = 4

... factorial at line 5 (996 times) ...

factorial at line 5
-----------
       4 | def factorial(x):
-->    5 |     return x * factorial(x - 1)
x = -993

除了处理重复框架外,我们还向FrameInfo.stack_data传递了跟踪记录对象而不是框架。

如果您愿意,可以传递collapse_repeated_frames=FalseFrameInfo.stack_data(而不是Options),这样它就会为整个堆栈产生FrameInfo对象。

项目详情


下载文件

下载您平台上的文件。如果您不确定选择哪一个,请了解有关安装包的更多信息。

源分布

stack_data-0.6.3.tar.gz (44.7 kB 查看哈希值)

上传时间:

构建分布

stack_data-0.6.3-py3-none-any.whl (24.5 kB 查看哈希值)

上传时间: Python 3

由以下支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误日志 StatusPage StatusPage 状态页