Skip to content

Commit

Permalink
Merge pull request #2754 from Textualize/jupyter-traceback-fix
Browse files Browse the repository at this point in the history
Fix tracebacks in Jupyter
  • Loading branch information
willmcgugan committed Jan 14, 2023
2 parents 80188f2 + 8baf90b commit b7ac6bb
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 27 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ 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).

## [13.1.0] - 2023-01-14

### Fixed

- Fixed wrong filenames in Jupyter tracebacks https://github.com/Textualize/rich/issues/2271

### Added

- Added locals_hide_dunder and locals_hide_sunder to Tracebacks, to hide double underscore and single underscore locals. https://github.com/Textualize/rich/pull/2754

### Changed

- Tracebacks will now hide double underscore names from locals by default. Set `locals_hide_dunder=False` to restore previous behaviour.

## [13.0.1] - 2023-01-06

### Fixed
Expand Down Expand Up @@ -1860,6 +1874,7 @@ Major version bump for a breaking change to `Text.stylize signature`, which corr

- First official release, API still to be stabilized

[13.1.0]: https://github.com/textualize/rich/compare/v13.0.1...v13.1.0
[13.0.1]: https://github.com/textualize/rich/compare/v13.0.0...v13.0.1
[13.0.0]: https://github.com/textualize/rich/compare/v12.6.0...v13.0.0
[12.6.0]: https://github.com/textualize/rich/compare/v12.5.2...v12.6.0
Expand Down
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/Textualize/rich"
documentation = "https://rich.readthedocs.io/en/latest/"
version = "13.0.1"
version = "13.1.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
authors = ["Will McGugan <willmcgugan@gmail.com>"]
license = "MIT"
Expand Down
117 changes: 91 additions & 26 deletions rich/traceback.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
from __future__ import absolute_import

import linecache
import os
import platform
import sys
from dataclasses import dataclass, field
from pathlib import Path
from traceback import walk_tb
from types import ModuleType, TracebackType
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Type, Union
from typing import (
Any,
Callable,
Dict,
Iterable,
List,
Optional,
Sequence,
Tuple,
Type,
Union,
)

from pygments.lexers import guess_lexer_for_filename
from pygments.token import Comment, Keyword, Name, Number, Operator, String
Expand Down Expand Up @@ -42,6 +53,10 @@ def install(
theme: Optional[str] = None,
word_wrap: bool = False,
show_locals: bool = False,
locals_max_length: int = LOCALS_MAX_LENGTH,
locals_max_string: int = LOCALS_MAX_STRING,
locals_hide_dunder: bool = True,
locals_hide_sunder: Optional[bool] = None,
indent_guides: bool = True,
suppress: Iterable[Union[str, ModuleType]] = (),
max_frames: int = 100,
Expand All @@ -59,6 +74,11 @@ def install(
a theme appropriate for the platform.
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.
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.
locals_hide_dunder (bool, optional): Hide locals prefixed with double underscore. Defaults to True.
locals_hide_sunder (bool, optional): Hide locals prefixed with single underscore. 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.
Expand All @@ -68,6 +88,12 @@ def install(
"""
traceback_console = Console(file=sys.stderr) if console is None else console

locals_hide_sunder = (
True
if (traceback_console.is_jupyter and locals_hide_sunder is None)
else locals_hide_sunder
)

def excepthook(
type_: Type[BaseException],
value: BaseException,
Expand All @@ -83,6 +109,10 @@ def excepthook(
theme=theme,
word_wrap=word_wrap,
show_locals=show_locals,
locals_max_length=locals_max_length,
locals_max_string=locals_max_string,
locals_hide_dunder=locals_hide_dunder,
locals_hide_sunder=bool(locals_hide_sunder),
indent_guides=indent_guides,
suppress=suppress,
max_frames=max_frames,
Expand Down Expand Up @@ -193,6 +223,8 @@ 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.
locals_hide_dunder (bool, optional): Hide locals prefixed with double underscore. Defaults to True.
locals_hide_sunder (bool, optional): Hide locals prefixed with single underscore. Defaults to False.
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.
Expand All @@ -209,14 +241,17 @@ class Traceback:
def __init__(
self,
trace: Optional[Trace] = None,
*,
width: Optional[int] = 100,
extra_lines: int = 3,
theme: Optional[str] = None,
word_wrap: bool = False,
show_locals: bool = False,
indent_guides: bool = True,
locals_max_length: int = LOCALS_MAX_LENGTH,
locals_max_string: int = LOCALS_MAX_STRING,
locals_hide_dunder: bool = True,
locals_hide_sunder: bool = False,
indent_guides: bool = True,
suppress: Iterable[Union[str, ModuleType]] = (),
max_frames: int = 100,
):
Expand All @@ -238,6 +273,8 @@ def __init__(
self.indent_guides = indent_guides
self.locals_max_length = locals_max_length
self.locals_max_string = locals_max_string
self.locals_hide_dunder = locals_hide_dunder
self.locals_hide_sunder = locals_hide_sunder

self.suppress: Sequence[str] = []
for suppress_entity in suppress:
Expand All @@ -258,14 +295,17 @@ def from_exception(
exc_type: Type[Any],
exc_value: BaseException,
traceback: Optional[TracebackType],
*,
width: Optional[int] = 100,
extra_lines: int = 3,
theme: Optional[str] = None,
word_wrap: bool = False,
show_locals: bool = False,
indent_guides: bool = True,
locals_max_length: int = LOCALS_MAX_LENGTH,
locals_max_string: int = LOCALS_MAX_STRING,
locals_hide_dunder: bool = True,
locals_hide_sunder: bool = False,
indent_guides: bool = True,
suppress: Iterable[Union[str, ModuleType]] = (),
max_frames: int = 100,
) -> "Traceback":
Expand All @@ -284,6 +324,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.
locals_hide_dunder (bool, optional): Hide locals prefixed with double underscore. Defaults to True.
locals_hide_sunder (bool, optional): Hide locals prefixed with single underscore. 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.
Expand All @@ -297,6 +339,8 @@ def from_exception(
show_locals=show_locals,
locals_max_length=locals_max_length,
locals_max_string=locals_max_string,
locals_hide_dunder=locals_hide_dunder,
locals_hide_sunder=locals_hide_sunder,
)
return cls(
rich_traceback,
Expand All @@ -308,6 +352,8 @@ def from_exception(
indent_guides=indent_guides,
locals_max_length=locals_max_length,
locals_max_string=locals_max_string,
locals_hide_dunder=locals_hide_dunder,
locals_hide_sunder=locals_hide_sunder,
suppress=suppress,
max_frames=max_frames,
)
Expand All @@ -318,9 +364,12 @@ def extract(
exc_type: Type[BaseException],
exc_value: BaseException,
traceback: Optional[TracebackType],
*,
show_locals: bool = False,
locals_max_length: int = LOCALS_MAX_LENGTH,
locals_max_string: int = LOCALS_MAX_STRING,
locals_hide_dunder: bool = True,
locals_hide_sunder: bool = False,
) -> Trace:
"""Extract traceback information.
Expand All @@ -332,6 +381,8 @@ def extract(
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.
locals_hide_dunder (bool, optional): Hide locals prefixed with double underscore. Defaults to True.
locals_hide_sunder (bool, optional): Hide locals prefixed with single underscore. Defaults to False.
Returns:
Trace: A Trace instance which you can use to construct a `Traceback`.
Expand Down Expand Up @@ -368,13 +419,28 @@ def safe_str(_object: Any) -> str:
stacks.append(stack)
append = stack.frames.append

def get_locals(
iter_locals: Iterable[Tuple[str, object]]
) -> Iterable[Tuple[str, object]]:
"""Extract locals from an iterator of key pairs."""
if not (locals_hide_dunder or locals_hide_sunder):
yield from iter_locals
return
for key, value in iter_locals:
if locals_hide_dunder and key.startswith("__"):
continue
if locals_hide_sunder and key.startswith("_"):
continue
yield key, value

for frame_summary, line_no in walk_tb(traceback):
filename = frame_summary.f_code.co_filename
if filename and not filename.startswith("<"):
if not os.path.isabs(filename):
filename = os.path.join(_IMPORT_CWD, filename)
if frame_summary.f_locals.get("_rich_traceback_omit", False):
continue

frame = Frame(
filename=filename or "?",
lineno=line_no,
Expand All @@ -385,7 +451,7 @@ def safe_str(_object: Any) -> str:
max_length=locals_max_length,
max_string=locals_max_string,
)
for key, value in frame_summary.f_locals.items()
for key, value in get_locals(frame_summary.f_locals.items())
}
if show_locals
else None,
Expand Down Expand Up @@ -500,13 +566,14 @@ def _render_syntax_error(self, syntax_error: _SyntaxError) -> RenderResult:
highlighter = ReprHighlighter()
path_highlighter = PathHighlighter()
if syntax_error.filename != "<stdin>":
text = Text.assemble(
(f" {syntax_error.filename}", "pygments.string"),
(":", "pygments.text"),
(str(syntax_error.lineno), "pygments.number"),
style="pygments.text",
)
yield path_highlighter(text)
if os.path.exists(syntax_error.filename):
text = Text.assemble(
(f" {syntax_error.filename}", "pygments.string"),
(":", "pygments.text"),
(str(syntax_error.lineno), "pygments.number"),
style="pygments.text",
)
yield path_highlighter(text)
syntax_error_text = highlighter(syntax_error.line.rstrip())
syntax_error_text.no_wrap = True
offset = min(syntax_error.offset - 1, len(syntax_error_text))
Expand Down Expand Up @@ -537,7 +604,6 @@ def _guess_lexer(cls, filename: str, code: str) -> str:
def _render_stack(self, stack: Stack) -> RenderResult:
path_highlighter = PathHighlighter()
theme = self.theme
code_cache: Dict[str, str] = {}

def read_code(filename: str) -> str:
"""Read files, and cache results on filename.
Expand All @@ -548,11 +614,7 @@ def read_code(filename: str) -> str:
Returns:
str: Contents of file
"""
code = code_cache.get(filename)
if code is None:
code = Path(filename).read_text(encoding="utf-8", errors="replace")
code_cache[filename] = code
return code
return "".join(linecache.getlines(filename))

def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]:
if frame.locals:
Expand Down Expand Up @@ -591,14 +653,17 @@ def render_locals(frame: Frame) -> Iterable[ConsoleRenderable]:
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"),
(str(frame.lineno), "pygments.number"),
" in ",
(frame.name, "pygments.function"),
style="pygments.text",
)
if os.path.exists(frame.filename):
text = Text.assemble(
path_highlighter(Text(frame.filename, style="pygments.string")),
(":", "pygments.text"),
(str(frame.lineno), "pygments.number"),
" in ",
(frame.name, "pygments.function"),
style="pygments.text",
)
else:
text = Text.assemble("in ", (frame.name, "pygments.function"))
if not frame.filename.startswith("<") and not first:
yield ""
yield text
Expand Down

0 comments on commit b7ac6bb

Please sign in to comment.