从Python堆栈帧和回溯中提取数据以进行信息显示
项目描述
stack_data
这是一个从堆栈帧和回溯中提取数据的库,尤其是显示比默认更有用的回溯。它为IPython和futurecoder中的回溯提供支持。
您可以从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.bar
或foo[bar]
这样的表达式。在这些情况下,name
是该表达式的源代码。《pure_eval》确保它只评估不会产生任何副作用的表达式,例如,当foo.bar
是一个正常的属性而不是一个描述符(如属性)时。
frame_info.variables
是frame_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> < 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为您完成。只需遵循以下步骤
- 单独作为它不是
stack_data
的依赖项,请使用pip install pygments
。 - 创建一个pygments格式化器对象,如
HtmlFormatter
或Terminal256Formatter
。 - 将格式化器传递到
Options
的pygments_formatter
参数中。 - 使用
line.render(pygmented=True)
来获取格式化的文本。在这种情况下,您不能向render
传递任何标记。
如果您愿意,还可以结合pygments语法高亮,在框架中突出显示正在执行的节点。为此,您需要
- 一个pygments样式 - 要么是一个样式类,要么是一个命名它的字符串。请参阅样式文档和样式画廊。
- 对执行节点样式进行的修改,例如字符串
"bold"
或"bg:#ffff00"
(黄色背景)。请参阅样式规则文档。 - 将这些内容传递给
stack_data.style_with_executing_node(style, modifier)
以获取一个新的样式类。 - 在创建格式化程序时传递新样式。
注意,这在与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=False
给FrameInfo.stack_data
(而不是Options
),这样它就会为整个堆栈产生FrameInfo
对象。
项目详情
下载文件
下载您平台上的文件。如果您不确定选择哪一个,请了解有关安装包的更多信息。
源分布
构建分布
stack_data-0.6.3.tar.gz的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9 |
|
MD5 | d04f7cda6589138e90691aec1edbf0d5 |
|
BLAKE2b-256 | 28e355dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b |
stack_data-0.6.3-py3-none-any.whl 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695 |
|
MD5 | b5179fcd8825a72548eb97bf7d0d609b |
|
BLAKE2b-256 | f17bce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91 |