METADATA 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. Metadata-Version: 2.1
  2. Name: stack-data
  3. Version: 0.6.3
  4. Summary: Extract data from python stack frames and tracebacks for informative displays
  5. Home-page: http://github.com/alexmojaki/stack_data
  6. Author: Alex Hall
  7. Author-email: alex.mojaki@gmail.com
  8. License: MIT
  9. Classifier: Intended Audience :: Developers
  10. Classifier: Programming Language :: Python :: 3.5
  11. Classifier: Programming Language :: Python :: 3.6
  12. Classifier: Programming Language :: Python :: 3.7
  13. Classifier: Programming Language :: Python :: 3.8
  14. Classifier: Programming Language :: Python :: 3.9
  15. Classifier: Programming Language :: Python :: 3.10
  16. Classifier: Programming Language :: Python :: 3.11
  17. Classifier: Programming Language :: Python :: 3.12
  18. Classifier: License :: OSI Approved :: MIT License
  19. Classifier: Operating System :: OS Independent
  20. Classifier: Topic :: Software Development :: Debuggers
  21. Description-Content-Type: text/markdown
  22. License-File: LICENSE.txt
  23. Requires-Dist: executing >=1.2.0
  24. Requires-Dist: asttokens >=2.1.0
  25. Requires-Dist: pure-eval
  26. Provides-Extra: tests
  27. Requires-Dist: pytest ; extra == 'tests'
  28. Requires-Dist: typeguard ; extra == 'tests'
  29. Requires-Dist: pygments ; extra == 'tests'
  30. Requires-Dist: littleutils ; extra == 'tests'
  31. Requires-Dist: cython ; extra == 'tests'
  32. # stack_data
  33. [![Tests](https://github.com/alexmojaki/stack_data/actions/workflows/pytest.yml/badge.svg)](https://github.com/alexmojaki/stack_data/actions/workflows/pytest.yml) [![Coverage Status](https://coveralls.io/repos/github/alexmojaki/stack_data/badge.svg?branch=master)](https://coveralls.io/github/alexmojaki/stack_data?branch=master) [![Supports Python versions 3.5+](https://img.shields.io/pypi/pyversions/stack_data.svg)](https://pypi.python.org/pypi/stack_data)
  34. This is a library that extracts data from stack frames and tracebacks, particularly to display more useful tracebacks than the default. It powers the tracebacks in IPython and [futurecoder](https://futurecoder.io/):
  35. ![futurecoder example](https://futurecoder.io/static/img/features/traceback.png)
  36. You can install it from PyPI:
  37. pip install stack_data
  38. ## Basic usage
  39. Here's some code we'd like to inspect:
  40. ```python
  41. def foo():
  42. result = []
  43. for i in range(5):
  44. row = []
  45. result.append(row)
  46. print_stack()
  47. for j in range(5):
  48. row.append(i * j)
  49. return result
  50. ```
  51. Note that `foo` calls a function `print_stack()`. In reality we can imagine that an exception was raised at this line, or a debugger stopped there, but this is easy to play with directly. Here's a basic implementation:
  52. ```python
  53. import inspect
  54. import stack_data
  55. def print_stack():
  56. frame = inspect.currentframe().f_back
  57. frame_info = stack_data.FrameInfo(frame)
  58. print(f"{frame_info.code.co_name} at line {frame_info.lineno}")
  59. print("-----------")
  60. for line in frame_info.lines:
  61. print(f"{'-->' if line.is_current else ' '} {line.lineno:4} | {line.render()}")
  62. ```
  63. (Beware that this has a major bug - it doesn't account for line gaps, which we'll learn about later)
  64. The output of one call to `print_stack()` looks like:
  65. ```
  66. foo at line 9
  67. -----------
  68. 6 | for i in range(5):
  69. 7 | row = []
  70. 8 | result.append(row)
  71. --> 9 | print_stack()
  72. 10 | for j in range(5):
  73. ```
  74. The code for `print_stack()` is fairly self-explanatory. If you want to learn more details about a particular class or method I suggest looking through some docstrings. `FrameInfo` is a class that accepts either a frame or a traceback object and provides a bunch of nice attributes and properties (which are cached so you don't need to worry about performance). In particular `frame_info.lines` is a list of `Line` objects. `line.render()` returns the source code of that line suitable for display. Without any arguments it simply strips any common leading indentation. Later on we'll see a more powerful use for it.
  75. You can see that `frame_info.lines` includes some lines of surrounding context. By default it includes 3 pieces of context before the main line and 1 piece after. We can configure the amount of context by passing options:
  76. ```python
  77. options = stack_data.Options(before=1, after=0)
  78. frame_info = stack_data.FrameInfo(frame, options)
  79. ```
  80. Then the output looks like:
  81. ```
  82. foo at line 9
  83. -----------
  84. 8 | result.append(row)
  85. --> 9 | print_stack()
  86. ```
  87. Note that these parameters are not the number of *lines* before and after to include, but the number of *pieces*. A piece is a range of one or more lines in a file that should logically be grouped together. A piece contains either a single simple statement or a part of a compound statement (loops, if, try/except, etc) that doesn't contain any other statements. Most pieces are a single line, but a multi-line statement or `if` condition is a single piece. In the example above, all pieces are one line, because nothing is spread across multiple lines. If we change our code to include some multiline bits:
  88. ```python
  89. def foo():
  90. result = []
  91. for i in range(5):
  92. row = []
  93. result.append(
  94. row
  95. )
  96. print_stack()
  97. for j in range(
  98. 5
  99. ):
  100. row.append(i * j)
  101. return result
  102. ```
  103. and then run the original code with the default options, then the output is:
  104. ```
  105. foo at line 11
  106. -----------
  107. 6 | for i in range(5):
  108. 7 | row = []
  109. 8 | result.append(
  110. 9 | row
  111. 10 | )
  112. --> 11 | print_stack()
  113. 12 | for j in range(
  114. 13 | 5
  115. 14 | ):
  116. ```
  117. Now lines 8-10 and lines 12-14 are each a single piece. Note that the output is essentially the same as the original in terms of the amount of code. The division of files into pieces means that the edge of the context is intuitive and doesn't crop out parts of statements or expressions. For example, if context was measured in lines instead of pieces, the last line of the above would be `for j in range(` which is much less useful.
  118. However, if a piece is very long, including all of it could be cumbersome. For this, `Options` has a parameter `max_lines_per_piece`, which is 6 by default. Suppose we have a piece in our code that's longer than that:
  119. ```python
  120. row = [
  121. 1,
  122. 2,
  123. 3,
  124. 4,
  125. 5,
  126. ]
  127. ```
  128. `frame_info.lines` will truncate this piece so that instead of 7 `Line` objects it will produce 5 `Line` objects and one `LINE_GAP` in the middle, making 6 objects in total for the piece. Our code doesn't currently handle gaps, so it will raise an exception. We can modify it like so:
  129. ```python
  130. for line in frame_info.lines:
  131. if line is stack_data.LINE_GAP:
  132. print(" (...)")
  133. else:
  134. print(f"{'-->' if line.is_current else ' '} {line.lineno:4} | {line.render()}")
  135. ```
  136. Now the output looks like:
  137. ```
  138. foo at line 15
  139. -----------
  140. 6 | for i in range(5):
  141. 7 | row = [
  142. 8 | 1,
  143. 9 | 2,
  144. (...)
  145. 12 | 5,
  146. 13 | ]
  147. 14 | result.append(row)
  148. --> 15 | print_stack()
  149. 16 | for j in range(5):
  150. ```
  151. Alternatively, you can flip the condition around and check `if isinstance(line, stack_data.Line):`. Either way, you should always check for line gaps, or your code may appear to work at first but fail when it encounters a long piece.
  152. Note that the executing piece, i.e. the piece containing the current line being executed (line 15 in this case) is never truncated, no matter how long it is.
  153. The lines of context never stray outside `frame_info.scope`, which is the innermost function or class definition containing the current line. For example, this is the output for a short function which has neither 3 lines before nor 1 line after the current line:
  154. ```
  155. bar at line 6
  156. -----------
  157. 4 | def bar():
  158. 5 | foo()
  159. --> 6 | print_stack()
  160. ```
  161. Sometimes it's nice to ensure that the function signature is always showing. This can be done with `Options(include_signature=True)`. The result looks like this:
  162. ```
  163. foo at line 14
  164. -----------
  165. 9 | def foo():
  166. (...)
  167. 11 | for i in range(5):
  168. 12 | row = []
  169. 13 | result.append(row)
  170. --> 14 | print_stack()
  171. 15 | for j in range(5):
  172. ```
  173. To avoid wasting space, pieces never start or end with a blank line, and blank lines between pieces are excluded. So if our code looks like this:
  174. ```python
  175. for i in range(5):
  176. row = []
  177. result.append(row)
  178. print_stack()
  179. for j in range(5):
  180. ```
  181. The output doesn't change much, except you can see jumps in the line numbers:
  182. ```
  183. 11 | for i in range(5):
  184. 12 | row = []
  185. 14 | result.append(row)
  186. --> 15 | print_stack()
  187. 17 | for j in range(5):
  188. ```
  189. ## Variables
  190. You can also inspect variables and other expressions in a frame, e.g:
  191. ```python
  192. for var in frame_info.variables:
  193. print(f"{var.name} = {repr(var.value)}")
  194. ```
  195. which may output:
  196. ```python
  197. result = [[0, 0, 0, 0, 0], [0, 1, 2, 3, 4], [0, 2, 4, 6, 8], [0, 3, 6, 9, 12], []]
  198. i = 4
  199. row = []
  200. j = 4
  201. ```
  202. `frame_info.variables` returns a list of `Variable` objects, which have attributes `name`, `value`, and `nodes`, which is a list of all AST representing that expression.
  203. A `Variable` may refer to an expression other than a simple variable name. It can be any expression evaluated by the library [`pure_eval`](https://github.com/alexmojaki/pure_eval) which it deems 'interesting' (see those docs for more info). This includes expressions like `foo.bar` or `foo[bar]`. In these cases `name` is the source code of that expression. `pure_eval` ensures that it only evaluates expressions that won't have any side effects, e.g. where `foo.bar` is a normal attribute rather than a descriptor such as a property.
  204. `frame_info.variables` is a list of all the interesting expressions found in `frame_info.scope`, e.g. the current function, which may include expressions not visible in `frame_info.lines`. You can restrict the list by using `frame_info.variables_in_lines` or even `frame_info.variables_in_executing_piece`. For more control you can use `frame_info.variables_by_lineno`. See the docstrings for more information.
  205. ## Rendering lines with ranges and markers
  206. Sometimes you may want to insert special characters into the text for display purposes, e.g. HTML or ANSI color codes. `stack_data` provides a few tools to make this easier.
  207. Let's say we have a `Line` object where `line.text` (the original raw source code of that line) is `"foo = bar"`, so `line.text[6:9]` is `"bar"`, and we want to emphasise that part by inserting HTML at positions 6 and 9 in the text. Here's how we can do that directly:
  208. ```python
  209. markers = [
  210. stack_data.MarkerInLine(position=6, is_start=True, string="<b>"),
  211. stack_data.MarkerInLine(position=9, is_start=False, string="</b>"),
  212. ]
  213. line.render(markers) # returns "foo = <b>bar</b>"
  214. ```
  215. Here `is_start=True` indicates that the marker is the first of a pair. This helps `line.render()` sort and insert the markers correctly so you don't end up with malformed HTML like `foo<b>.<i></b>bar</i>` where tags overlap.
  216. Since we're inserting HTML, we should actually use `line.render(markers, escape_html=True)` which will escape special HTML characters in the Python source (but not the markers) so for example `foo = bar < spam` would be rendered as `foo = <b>bar</b> &lt; spam`.
  217. Usually though you wouldn't create markers directly yourself. Instead you would start with one or more ranges and then convert them, like so:
  218. ```python
  219. ranges = [
  220. stack_data.RangeInLine(start=0, end=3, data="foo"),
  221. stack_data.RangeInLine(start=6, end=9, data="bar"),
  222. ]
  223. def convert_ranges(r):
  224. if r.data == "bar":
  225. return "<b>", "</b>"
  226. # This results in `markers` being the same as in the above example.
  227. markers = stack_data.markers_from_ranges(ranges, convert_ranges)
  228. ```
  229. `RangeInLine` has a `data` attribute which can be any object. `markers_from_ranges` accepts a converter function to which it passes all the `RangeInLine` objects. If the converter function returns a pair of strings, it creates two markers from them. Otherwise it should return `None` to indicate that the range should be ignored, as with the first range containing `"foo"` in this example.
  230. The reason this is useful is because there are built in tools to create these ranges for you. For example, if we change our `print_stack()` function to contain this:
  231. ```python
  232. def convert_variable_ranges(r):
  233. variable, _node = r.data
  234. return f'<span data-value="{repr(variable.value)}">', '</span>'
  235. markers = stack_data.markers_from_ranges(line.variable_ranges, convert_variable_ranges)
  236. print(f"{'-->' if line.is_current else ' '} {line.lineno:4} | {line.render(markers, escape_html=True)}")
  237. ```
  238. Then the output becomes:
  239. ```
  240. foo at line 15
  241. -----------
  242. 9 | def foo():
  243. (...)
  244. 11 | for <span data-value="4">i</span> in range(5):
  245. 12 | <span data-value="[]">row</span> = []
  246. 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>)
  247. --> 15 | print_stack()
  248. 17 | for <span data-value="4">j</span> in range(5):
  249. ```
  250. `line.variable_ranges` is a list of RangeInLines for each Variable that appears at least partially in this line. The data attribute of the range is a pair `(variable, node)` where node is the particular AST node from the list `variable.nodes` that corresponds to this range.
  251. You can also use `line.token_ranges` (e.g. if you want to do your own syntax highlighting) or `line.executing_node_ranges` if you want to highlight the currently executing node identified by the [`executing`](https://github.com/alexmojaki/executing) library. Or if you want to make your own range from an AST node, use `line.range_from_node(node, data)`. See the docstrings for more info.
  252. ### Syntax highlighting with Pygments
  253. If you'd like pretty colored text without the work, you can let [Pygments](https://pygments.org/) do it for you. Just follow these steps:
  254. 1. `pip install pygments` separately as it's not a dependency of `stack_data`.
  255. 2. Create a pygments formatter object such as `HtmlFormatter` or `Terminal256Formatter`.
  256. 3. Pass the formatter to `Options` in the argument `pygments_formatter`.
  257. 4. Use `line.render(pygmented=True)` to get your formatted text. In this case you can't pass any markers to `render`.
  258. If you want, you can also highlight the executing node in the frame in combination with the pygments syntax highlighting. For this you will need:
  259. 1. A pygments style - either a style class or a string that names it. See the [documentation on styles](https://pygments.org/docs/styles/) and the [styles gallery](https://blog.yjl.im/2015/08/pygments-styles-gallery.html).
  260. 2. A modification to make to the style for the executing node, which is a string such as `"bold"` or `"bg:#ffff00"` (yellow background). See the [documentation on style rules](https://pygments.org/docs/styles/#style-rules).
  261. 3. Pass these two things to `stack_data.style_with_executing_node(style, modifier)` to get a new style class.
  262. 4. Pass the new style to your formatter when you create it.
  263. Note that this doesn't work with `TerminalFormatter` which just uses the basic ANSI colors and doesn't use the style passed to it in general.
  264. ## Getting the full stack
  265. Currently `print_stack()` doesn't actually print the stack, it just prints one frame. Instead of `frame_info = FrameInfo(frame, options)`, let's do this:
  266. ```python
  267. for frame_info in FrameInfo.stack_data(frame, options):
  268. ```
  269. Now the output looks something like this:
  270. ```
  271. <module> at line 18
  272. -----------
  273. 14 | for j in range(5):
  274. 15 | row.append(i * j)
  275. 16 | return result
  276. --> 18 | bar()
  277. bar at line 5
  278. -----------
  279. 4 | def bar():
  280. --> 5 | foo()
  281. foo at line 13
  282. -----------
  283. 10 | for i in range(5):
  284. 11 | row = []
  285. 12 | result.append(row)
  286. --> 13 | print_stack()
  287. 14 | for j in range(5):
  288. ```
  289. However, just as `frame_info.lines` doesn't always yield `Line` objects, `FrameInfo.stack_data` doesn't always yield `FrameInfo` objects, and we must modify our code to handle that. Let's look at some different sample code:
  290. ```python
  291. def factorial(x):
  292. return x * factorial(x - 1)
  293. try:
  294. print(factorial(5))
  295. except:
  296. print_stack()
  297. ```
  298. In this code we've forgotten to include a base case in our `factorial` function so it will fail with a `RecursionError` and there'll be many frames with similar information. Similar to the built in Python traceback, `stack_data` avoids showing all of these frames. Instead you will get a `RepeatedFrames` object which summarises the information. See its docstring for more details.
  299. Here is our updated implementation:
  300. ```python
  301. def print_stack():
  302. for frame_info in FrameInfo.stack_data(sys.exc_info()[2]):
  303. if isinstance(frame_info, FrameInfo):
  304. print(f"{frame_info.code.co_name} at line {frame_info.lineno}")
  305. print("-----------")
  306. for line in frame_info.lines:
  307. print(f"{'-->' if line.is_current else ' '} {line.lineno:4} | {line.render()}")
  308. for var in frame_info.variables:
  309. print(f"{var.name} = {repr(var.value)}")
  310. print()
  311. else:
  312. print(f"... {frame_info.description} ...\n")
  313. ```
  314. And the output:
  315. ```
  316. <module> at line 9
  317. -----------
  318. 4 | def factorial(x):
  319. 5 | return x * factorial(x - 1)
  320. 8 | try:
  321. --> 9 | print(factorial(5))
  322. 10 | except:
  323. factorial at line 5
  324. -----------
  325. 4 | def factorial(x):
  326. --> 5 | return x * factorial(x - 1)
  327. x = 5
  328. factorial at line 5
  329. -----------
  330. 4 | def factorial(x):
  331. --> 5 | return x * factorial(x - 1)
  332. x = 4
  333. ... factorial at line 5 (996 times) ...
  334. factorial at line 5
  335. -----------
  336. 4 | def factorial(x):
  337. --> 5 | return x * factorial(x - 1)
  338. x = -993
  339. ```
  340. In addition to handling repeated frames, we've passed a traceback object to `FrameInfo.stack_data` instead of a frame.
  341. If you want, you can pass `collapse_repeated_frames=False` to `FrameInfo.stack_data` (not to `Options`) and it will just yield `FrameInfo` objects for the full stack.