Skip to content

Commit

Permalink
theme stack
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Oct 2, 2020
1 parent 42cbd35 commit 2634553
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 30 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Added Console.bell method
- Added Set to types that Console.print will automatically pretty print
- Added show_locals to Traceback
- Added theme stack mechanism, see Console.push_theme and Console.pop_theme

### Changed

Expand Down
6 changes: 3 additions & 3 deletions 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 = "7.1.0"
version = "8.0.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 Expand Up @@ -37,5 +37,5 @@ jupyter = ["ipywidgets"]
[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry-core>=1.0.0a9"]
build-backend = "poetry.core.masonry.api"
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
black==20.8b1
mypy==0.782
poetry==1.0.10
pytest==5.4.3
pytest==6.1.0
pytest-cov==2.10.1
13 changes: 12 additions & 1 deletion rich/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@


def get_console() -> "Console":
"""Get a global Console instance.
"""Get a global :class:`~rich.console.Console` instance. This function is used when Rich requires a Console,
and hasn't been explicitly given one.
Returns:
Console: A console instance.
Expand All @@ -31,6 +32,16 @@ def print(
file: IO[str] = None,
flush: bool = False,
):
"""Print object(s) supplied via positional arguments. This function has an identical signature
to the built-in print. For more advanced features, see the :class:`~rich.console.Console` class.
Args:
sep (str, optional): Separator between printed objects. Defaults to " ".
end (str, optional): Character to write at end of output. Defaults to "\n".
file (IO[str], optional): File to write to, or None for stdout. Defaults to None.
flush (bool, optional): Has no effect as Rich always flushes output.. Defaults to False.
"""
from .console import Console

write_console = get_console() if file is None else Console(file=file)
Expand Down
49 changes: 46 additions & 3 deletions rich/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from .style import Style
from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme
from .text import Text, TextType
from .theme import Theme
from .theme import Theme, ThemeStack

if TYPE_CHECKING:
from ._windows import WindowsConsoleFeatures
Expand Down Expand Up @@ -182,6 +182,22 @@ def get(self) -> str:
return self._result


class ThemeContext:
"""A context manager to use a temporary theme. See :meth:`~rich.console.Console.theme` for usage."""

def __init__(self, console: "Console", theme: Theme, inherit: bool = True) -> None:
self.console = console
self.theme = theme
self.inherit = inherit

def __enter__(self) -> "ThemeContext":
self.console.push_theme(self.theme)
return self

def __exit__(self, exc_type, exc_val, exc_tb) -> None:
self.console.pop_theme()


class RenderGroup:
"""Takes a group of renderables and returns a renderable object that renders the group.
Expand Down Expand Up @@ -372,7 +388,7 @@ def __init__(
if self.is_jupyter:
width = width or 93
height = height or 100
self._styles = themes.DEFAULT.styles if theme is None else theme.styles
self._theme_stack = ThemeStack(themes.DEFAULT if theme is None else theme)
self._width = width
self._height = height
self.tab_size = tab_size
Expand Down Expand Up @@ -498,6 +514,33 @@ def end_capture(self) -> str:
self._exit_buffer()
return render_result

def push_theme(self, theme: Theme, inherit: bool = True) -> None:
"""Push a new theme on to the top of the stack, overwriting the styles from the previous theme.
Generally speaking, you call :meth:`~rich.console.Console.theme` to get a context manager, rather
than calling this method directly.
Args:
theme (Theme): A theme instance.
inherit (bool, optional): Inherit existing styles. Defaults to True.
"""
self._theme_stack.push_theme(theme, inherit=inherit)

def pop_theme(self) -> None:
"""Remove theme from top of stack, restoring previous theme."""
self._theme_stack.pop_theme()

def theme(self, theme: Theme, inherit: bool = True) -> ThemeContext:
"""Use a new temporary theme for the duration of the context manager.
Args:
theme (Theme): Theme instance to user.
inherit (bool, optional): Inherit existing styles. Defaults to True.
Returns:
ThemeContext: [description]
"""
return ThemeContext(self, theme, inherit)

@property
def color_system(self) -> Optional[str]:
"""Get color system string.
Expand Down Expand Up @@ -785,7 +828,7 @@ def get_style(
return name

try:
style = self._styles.get(name)
style = self._theme_stack.get(name)
if style is None:
style = Style.parse(name)
return style.copy() if style.link else style
Expand Down
3 changes: 2 additions & 1 deletion rich/progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def run(self) -> None:
if last_completed != completed:
advance(task_id, completed - last_completed)
last_completed = completed

self.progress.update(self.task_id, completed=self.completed, refresh=True)

def __enter__(self) -> "_TrackThread":
Expand Down Expand Up @@ -780,7 +781,7 @@ def advance(self, task_id: TaskID, advance: float = 1) -> None:
popleft = _progress.popleft
while _progress and _progress[0].timestamp < old_sample_time:
popleft()
while len(_progress) > 10:
while len(_progress) > 1000:
popleft()
_progress.append(ProgressSample(current_time, update_completed))

Expand Down
44 changes: 42 additions & 2 deletions rich/theme.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import configparser
from typing import IO, Mapping
from typing import Dict, List, IO, Mapping, Optional

from .default_styles import DEFAULT_STYLES
from .style import Style, StyleType
Expand All @@ -9,10 +9,12 @@ class Theme:
"""A container for style information, used by :class:`~rich.console.Console`.
Args:
styles (Dict[str, Style], optional): A mapping of style names on to styles. Defaults to None for empty styles.
styles (Dict[str, Style], optional): A mapping of style names on to styles. Defaults to None for a theme with no styles.
inherit (bool, optional): Inherit default styles. Defaults to True.
"""

styles: Dict[str, Style]

def __init__(self, styles: Mapping[str, StyleType] = None, inherit: bool = True):
self.styles = DEFAULT_STYLES.copy() if inherit else {}
if styles is not None:
Expand Down Expand Up @@ -66,6 +68,44 @@ def read(cls, path: str, inherit: bool = True) -> "Theme":
return cls.from_file(config_file, source=path, inherit=inherit)


class ThemeStackError(Exception):
"""Base exception for errors related to the theme stack."""


class ThemeStack:
"""A stack of themes.
Args:
theme (Theme): A theme instance
"""

def __init__(self, theme: Theme) -> None:
self._entries: List[Dict[str, Style]] = [theme.styles]
self.get = self._entries[-1].get

def push_theme(self, theme: Theme, inherit: bool = True) -> None:
"""Push a theme on the top of the stack.
Args:
theme (Theme): A Theme instance.
inherit (boolean, optional): Inherit styles from current top of stack.
"""
styles: Dict[str, Style]
if inherit:
styles = {**self._entries[-1], **theme.styles}
else:
styles = theme.styles.copy()
self._entries.append(styles)
self.get = self._entries[-1].get

def pop_theme(self) -> None:
"""Pop (and discard) the top-most theme."""
if len(self._entries) == 1:
raise ThemeStackError("Unable to pop base theme")
self._entries.pop()
self.get = self._entries[-1].get


if __name__ == "__main__": # pragma: no cover
theme = Theme()
print(theme.config)
36 changes: 30 additions & 6 deletions rich/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from types import TracebackType
from typing import Any, Callable, Dict, List, Optional, Type

from pygments.lexers import guess_lexer_for_filename
from pygments.token import Token, String, Name, Number

from ._loop import loop_first, loop_last
Expand Down Expand Up @@ -335,6 +336,24 @@ def _render_syntax_error(
def _render_stack(self, stack: Stack, styles: Dict[str, Style]) -> 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.
Args:
filename (str): Filename to read
Returns:
str: Contents of file
"""
code = code_cache.get(filename)
if code is None:
with open(filename, "rt") as code_file:
code = code_file.read()
code_cache[filename] = code
return code

for first, frame in loop_first(stack.frames):
text = Text.assemble(
path_highlighter(Text(frame.filename, style=styles["string"])),
Expand All @@ -350,8 +369,12 @@ def _render_stack(self, stack: Stack, styles: Dict[str, Style]) -> RenderResult:
if frame.filename.startswith("<"):
continue
try:
syntax = Syntax.from_path(
frame.filename,
code = read_code(frame.filename)
lexer = guess_lexer_for_filename(frame.filename, code)
lexer_name = lexer.name
syntax = Syntax(
code,
lexer_name,
theme=theme,
line_numbers=True,
line_range=(
Expand All @@ -366,16 +389,17 @@ def _render_stack(self, stack: Stack, styles: Dict[str, Style]) -> RenderResult:
except Exception:
pass
else:
if frame.locals:
yield Columns(
yield (
Columns(
[
syntax,
render_scope(frame.locals, title="locals"),
],
padding=1,
)
else:
yield syntax
if frame.locals
else syntax
)


if __name__ == "__main__": # pragma: no cover
Expand Down
13 changes: 0 additions & 13 deletions tests/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,6 @@
)


def make_log():
log.debug("foo")
render = handler.console.file.getvalue()
return render


def test_log():
render = make_log()
print(repr(render))
expected = "\x1b[2;36m[DATE]\x1b[0m\x1b[2;36m \x1b[0m\x1b[32mDEBUG\x1b[0m foo \x1b[2mtest_logging.py\x1b[0m\x1b[2m:29\x1b[0m\n"
assert render == expected


@skip_win
def test_exception():
console = Console(
Expand Down

0 comments on commit 2634553

Please sign in to comment.