Skip to content

Commit

Permalink
Merge pull request #1519 from willmcgugan/suppress-frames
Browse files Browse the repository at this point in the history
Suppress frames and max_frames
  • Loading branch information
willmcgugan authored Sep 24, 2021
2 parents 522f841 + b82d756 commit cb8aeba
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 40 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [10.11.0] - 2021-09-24

### Added

- Added `suppress` parameter to tracebacks
- Added `max_frames` parameter to tracebacks

## [10.10.0] - 2021-09-18

### Added
Expand Down
39 changes: 39 additions & 0 deletions docs/source/traceback.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,42 @@ Rich can be installed as the default traceback handler so that all uncaught exce
install(show_locals=True)

There are a few options to configure the traceback handler, see :func:`~rich.traceback.install` for details.


Suppressing Frames
------------------

If you are working with a framework (click, django etc), you may only be interested in displaying code in your own application. You can exclude frameworks by setting the `suppress` argument on `Traceback`, `install`, and `print_exception`, which may be a iterable of modules or str paths.

Here's how you would exclude [click](https://click.palletsprojects.com/en/8.0.x/) from Rich exceptions::

import click
from rich.traceback import install
install(suppress=[click])

Suppressed frames will show the line and file only, without any code.

Max Frames
----------

A recursion error can generate very large tracebacks that take a while to render and contain a lot of repetitive frames. Rich guards against this with a `max_frames` argument, which defaults to 100. If a traceback contains more than 100 frames then only the first 50, and last 50 will be shown. You can disable this feature by setting `max_frames` to 0.

Here's an example of printing an recursive error::

from rich.console import Console


def foo(n):
return bar(n)


def bar(n):
return foo(n)


console = Console()

try:
foo(1)
except Exception:
console.print_exception(max_frames=20)
25 changes: 25 additions & 0 deletions examples/recursive_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""
Demonstrates Rich tracebacks for recursion errors.
Rich can exclude frames in the middle to avoid huge tracebacks.
"""

from rich.console import Console


def foo(n):
return bar(n)


def bar(n):
return foo(n)


console = Console()

try:
foo(1)
except Exception:
console.print_exception(max_frames=20)
23 changes: 23 additions & 0 deletions examples/suppress.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
try:
import click
except ImportError:
print("Please install click for this example")
print(" pip install click")
exit()

from rich.traceback import install

install(suppress=[click])


@click.command()
@click.option("--count", default=1, help="Number of greetings.")
def hello(count):
"""Simple program that greets NAME for a total of COUNT times."""
1 / 0
for x in range(count):
click.echo(f"Hello {name}!")


if __name__ == "__main__":
hello()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "rich"
homepage = "https://github.com/willmcgugan/rich"
documentation = "https://rich.readthedocs.io/en/latest/"
version = "10.10.0"
version = "10.11.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
authors = ["Will McGugan <[email protected]>"]
license = "MIT"
Expand Down
8 changes: 7 additions & 1 deletion rich/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from inspect import isclass
from itertools import islice
from time import monotonic
from types import FrameType, TracebackType
from types import FrameType, TracebackType, ModuleType
from typing import (
IO,
TYPE_CHECKING,
Expand Down Expand Up @@ -1704,6 +1704,8 @@ def print_exception(
theme: Optional[str] = None,
word_wrap: bool = False,
show_locals: bool = False,
suppress: Iterable[Union[str, ModuleType]] = (),
max_frames: int = 100,
) -> None:
"""Prints a rich render of the last exception and traceback.
Expand All @@ -1713,6 +1715,8 @@ def print_exception(
theme (str, optional): Override pygments theme used in traceback
word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
show_locals (bool, optional): Enable display of local variables. Defaults to False.
suppress (Iterable[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100.
"""
from .traceback import Traceback

Expand All @@ -1722,6 +1726,8 @@ def print_exception(
theme=theme,
word_wrap=word_wrap,
show_locals=show_locals,
suppress=suppress,
max_frames=max_frames,
)
self.print(traceback)

Expand Down
3 changes: 2 additions & 1 deletion rich/pretty.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ def install(
max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to None.
max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to None.
expand_all (bool, optional): Expand all containers. Defaults to False
expand_all (bool, optional): Expand all containers. Defaults to False.
max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100.
"""
from rich import get_console

Expand Down
127 changes: 90 additions & 37 deletions rich/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import sys
from dataclasses import dataclass, field
from traceback import walk_tb
from types import TracebackType
from typing import Any, Callable, Dict, Iterable, List, Optional, Type
from types import ModuleType, TracebackType
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Type, Union

from pygments.lexers import guess_lexer_for_filename
from pygments.token import Comment, Keyword, Name, Number, Operator, String
Expand Down Expand Up @@ -47,6 +47,8 @@ def install(
word_wrap: bool = False,
show_locals: bool = False,
indent_guides: bool = True,
suppress: Iterable[Union[str, ModuleType]] = (),
max_frames: int = 100,
) -> Callable[[Type[BaseException], BaseException, Optional[TracebackType]], Any]:
"""Install a rich traceback handler.
Expand All @@ -62,6 +64,7 @@ def install(
word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
show_locals (bool, optional): Enable display of local variables. Defaults to False.
indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True.
suppress (Sequence[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
Returns:
Callable: The previous exception handler that was replaced.
Expand All @@ -85,6 +88,8 @@ def excepthook(
word_wrap=word_wrap,
show_locals=show_locals,
indent_guides=indent_guides,
suppress=suppress,
max_frames=max_frames,
)
)

Expand Down Expand Up @@ -192,6 +197,9 @@ class Traceback:
locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to 10.
locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
suppress (Sequence[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100.
"""

LEXERS = {
Expand All @@ -213,6 +221,8 @@ def __init__(
indent_guides: bool = True,
locals_max_length: int = LOCALS_MAX_LENGTH,
locals_max_string: int = LOCALS_MAX_STRING,
suppress: Iterable[Union[str, ModuleType]] = (),
max_frames: int = 100,
):
if trace is None:
exc_type, exc_value, traceback = sys.exc_info()
Expand All @@ -233,6 +243,16 @@ def __init__(
self.locals_max_length = locals_max_length
self.locals_max_string = locals_max_string

self.suppress: Sequence[str] = []
for suppress_entity in suppress:
if not isinstance(suppress_entity, str):
path = os.path.dirname(suppress_entity.__file__)
else:
path = suppress_entity
path = os.path.normpath(os.path.abspath(path))
self.suppress.append(path)
self.max_frames = max(4, max_frames) if max_frames > 0 else 0

@classmethod
def from_exception(
cls,
Expand All @@ -247,6 +267,8 @@ def from_exception(
indent_guides: bool = True,
locals_max_length: int = LOCALS_MAX_LENGTH,
locals_max_string: int = LOCALS_MAX_STRING,
suppress: Iterable[Union[str, ModuleType]] = (),
max_frames: int = 100,
) -> "Traceback":
"""Create a traceback from exception info
Expand All @@ -263,6 +285,8 @@ def from_exception(
locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to 10.
locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
suppress (Iterable[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
max_frames (int): Maximum number of frames to show in a traceback, 0 for no maximum. Defaults to 100.
Returns:
Traceback: A Traceback instance that may be printed.
Expand All @@ -280,6 +304,8 @@ def from_exception(
indent_guides=indent_guides,
locals_max_length=locals_max_length,
locals_max_string=locals_max_string,
suppress=suppress,
max_frames=max_frames,
)

@classmethod
Expand Down Expand Up @@ -538,7 +564,33 @@ def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]:
max_string=self.locals_max_string,
)

for first, frame in loop_first(stack.frames):
exclude_frames: Optional[range] = None
if self.max_frames != 0:
exclude_frames = range(
self.max_frames // 2,
len(stack.frames) - self.max_frames // 2,
)

excluded = False
for frame_index, frame in enumerate(stack.frames):

if exclude_frames and frame_index in exclude_frames:
excluded = True
continue

if excluded:
assert exclude_frames is not None
yield Text(
f"\n... {len(exclude_frames)} frames hidden ...",
justify="center",
style="traceback.error",
)
excluded = False

first = frame_index == 1
frame_filename = frame.filename
suppressed = any(frame_filename.startswith(path) for path in self.suppress)

text = Text.assemble(
path_highlighter(Text(frame.filename, style="pygments.string")),
(":", "pygments.text"),
Expand All @@ -553,41 +605,42 @@ def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]:
if frame.filename.startswith("<"):
yield from render_locals(frame)
continue
try:
code = read_code(frame.filename)
lexer_name = self._guess_lexer(frame.filename, code)
syntax = Syntax(
code,
lexer_name,
theme=theme,
line_numbers=True,
line_range=(
frame.lineno - self.extra_lines,
frame.lineno + self.extra_lines,
),
highlight_lines={frame.lineno},
word_wrap=self.word_wrap,
code_width=88,
indent_guides=self.indent_guides,
dedent=False,
)
yield ""
except Exception as error:
yield Text.assemble(
(f"\n{error}", "traceback.error"),
)
else:
yield (
Columns(
[
syntax,
*render_locals(frame),
],
padding=1,
if not suppressed:
try:
code = read_code(frame.filename)
lexer_name = self._guess_lexer(frame.filename, code)
syntax = Syntax(
code,
lexer_name,
theme=theme,
line_numbers=True,
line_range=(
frame.lineno - self.extra_lines,
frame.lineno + self.extra_lines,
),
highlight_lines={frame.lineno},
word_wrap=self.word_wrap,
code_width=88,
indent_guides=self.indent_guides,
dedent=False,
)
yield ""
except Exception as error:
yield Text.assemble(
(f"\n{error}", "traceback.error"),
)
else:
yield (
Columns(
[
syntax,
*render_locals(frame),
],
padding=1,
)
if frame.locals
else syntax
)
if frame.locals
else syntax
)


if __name__ == "__main__": # pragma: no cover
Expand Down
Loading

0 comments on commit cb8aeba

Please sign in to comment.