From ebb4eaa26f6c557daf4d03540f21ea277a02824e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sat, 3 Oct 2020 12:06:19 +0100 Subject: [PATCH] themed tracebacks --- docs/source/introduction.rst | 2 +- rich/console.py | 22 ++++--- rich/default_styles.py | 19 +++--- rich/logging.py | 4 +- rich/panel.py | 17 +++-- rich/pretty.py | 1 + rich/progress.py | 2 +- rich/style.py | 39 +++++++----- rich/syntax.py | 9 ++- rich/table.py | 14 +++-- rich/theme.py | 7 +-- rich/traceback.py | 117 ++++++++++++++++++++++------------- tests/test_log.py | 6 +- tests/test_syntax.py | 24 +++++++ tests/test_theme.py | 17 ++++- 15 files changed, 193 insertions(+), 107 deletions(-) diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index 811937ffc..ace65bdbc 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -34,7 +34,7 @@ If you intend to use Rich with Jupyter then there are some additional dependenci Quick Start ----------- -The quickest way to get up and running with Rich is to import the alternative ``print`` function which may be used as a drop-in replacement for Python's built in function. Here's how you would do that:: +The quickest way to get up and running with Rich is to import the alternative ``print`` function which takes the same arguments as the built-in ``print`` and may be used as a drop-in replacement. Here's how you would do that:: from rich import print diff --git a/rich/console.py b/rich/console.py index f9847d2e4..e691a1912 100644 --- a/rich/console.py +++ b/rich/console.py @@ -514,9 +514,9 @@ 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 + def push_theme(self, theme: Theme, *, inherit: bool = True) -> None: + """Push a new theme on to the top of the stack, replacing the styles from the previous theme. + Generally speaking, you should call :meth:`~rich.console.Console.use_theme` to get a context manager, rather than calling this method directly. Args: @@ -529,12 +529,12 @@ 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. + def use_theme(self, theme: Theme, *, inherit: bool = True) -> ThemeContext: + """Use a different theme for the duration of the context manager. Args: theme (Theme): Theme instance to user. - inherit (bool, optional): Inherit existing styles. Defaults to True. + inherit (bool, optional): Inherit existing console styles. Defaults to True. Returns: ThemeContext: [description] @@ -1023,10 +1023,11 @@ def print( def print_exception( self, *, - width: Optional[int] = 88, + width: Optional[int] = 100, extra_lines: int = 3, theme: Optional[str] = None, word_wrap: bool = False, + show_locals: bool = False, ) -> None: """Prints a rich render of the last exception and traceback. @@ -1035,11 +1036,16 @@ def print_exception( extra_lines (int, optional): Additional lines of code to render. Defaults to 3. 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. """ from .traceback import Traceback traceback = Traceback( - width=width, extra_lines=extra_lines, theme=theme, word_wrap=word_wrap + width=width, + extra_lines=extra_lines, + theme=theme, + word_wrap=word_wrap, + show_locals=show_locals, ) self.print(traceback) diff --git a/rich/default_styles.py b/rich/default_styles.py index 8125b116a..e320e3cad 100644 --- a/rich/default_styles.py +++ b/rich/default_styles.py @@ -3,7 +3,7 @@ from .style import Style DEFAULT_STYLES: Dict[str, Style] = { - "none": Style(), + "none": Style.null(), "reset": Style( color="default", bgcolor="default", @@ -51,9 +51,9 @@ "logging.level.warning": Style(color="red"), "logging.level.error": Style(color="red", bold=True), "logging.level.critical": Style(color="red", bold=True, reverse=True), - "log.level": Style(), + "log.level": Style.null(), "log.time": Style(color="cyan", dim=True), - "log.message": Style(), + "log.message": Style.null(), "log.path": Style(dim=True), "repr.error": Style(color="red", bold=True), "repr.str": Style(color="green", italic=False, bold=False), @@ -75,12 +75,13 @@ "repr.url": Style(underline=True, color="bright_blue", italic=False, bold=False), "repr.uuid": Style(color="bright_yellow", bold=False), "rule.line": Style(color="bright_green"), - "rule.text": Style(), - "prompt": Style(), + "rule.text": Style.null(), + "prompt": Style.null(), "prompt.choices": Style(color="magenta", bold=True), "prompt.default": Style(color="cyan", bold=True), "prompt.invalid": Style(color="red"), "prompt.invalid.choice": Style(color="red"), + "pretty": Style.null(), "scope.border": Style(color="blue"), "scope.key": Style(color="yellow", italic=True), "scope.key.special": Style(color="yellow", italic=True, dim=True), @@ -89,21 +90,21 @@ "repr.filename": Style(color="bright_magenta"), "table.header": Style(bold=True), "table.footer": Style(bold=True), - "table.cell": Style(), + "table.cell": Style.null(), "table.title": Style(italic=True), "table.caption": Style(italic=True, dim=True), "traceback.border.syntax_error": Style(color="bright_red"), "traceback.border": Style(color="red"), - "traceback.text": Style(), + "traceback.text": Style.null(), "traceback.title": Style(color="red", bold=True), "traceback.exc_type": Style(color="bright_red", bold=True), - "traceback.exc_value": Style(), + "traceback.exc_value": Style.null(), "traceback.offset": Style(color="bright_red", bold=True), "bar.back": Style(color="grey23"), "bar.complete": Style(color="rgb(249,38,114)"), "bar.finished": Style(color="rgb(114,156,31)"), "bar.pulse": Style(color="rgb(249,38,114)"), - "progress.description": Style(), + "progress.description": Style.null(), "progress.filesize": Style(color="green"), "progress.filesize.total": Style(color="green"), "progress.download": Style(color="green"), diff --git a/rich/logging.py b/rich/logging.py index 4f58c6a71..fbce9d4da 100644 --- a/rich/logging.py +++ b/rich/logging.py @@ -31,7 +31,7 @@ class RichHandler(Handler): highlighter (Highlighter, optional): Highlighter to style log messages, or None to use ReprHighlighter. Defaults to None. markup (bool, optional): Enable console markup in log messages. Defaults to False. rich_tracebacks (bool, optional): Enable rich tracebacks with syntax highlighting and formatting. Defaults to False. - tracebacks_width (Optional[int], optional): Number of characters used to render tracebacks code. Defaults to 88. + tracebacks_width (Optional[int], optional): Number of characters used to render tracebacks, or None for full width. Defaults to None. tracebacks_extra_lines (int, optional): Additional lines of code to render tracebacks, or None for full width. Defaults to None. tracebacks_theme (str, optional): Override pygments theme used in traceback. tracebacks_word_wrap (bool, optional): Enable word wrapping of long tracebacks lines. Defaults to False. @@ -62,7 +62,7 @@ def __init__( highlighter: Highlighter = None, markup: bool = False, rich_tracebacks: bool = False, - tracebacks_width: Optional[int] = 88, + tracebacks_width: Optional[int] = None, tracebacks_extra_lines: int = 3, tracebacks_theme: Optional[str] = None, tracebacks_word_wrap: bool = True, diff --git a/rich/panel.py b/rich/panel.py index 1eb8b1fbb..5cb550695 100644 --- a/rich/panel.py +++ b/rich/panel.py @@ -155,15 +155,14 @@ def __rich_console__( def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement": _title = self._title - if _title is None: - width = Measurement.get(console, self.renderable, max_width - 2).maximum + 2 - else: - width = ( - measure_renderables( - console, [self.renderable, _title], max_width - ).maximum - + 4 - ) + _, right, _, left = Padding.unpack(self.padding) + padding = left + right + renderables = [self.renderable, _title] if _title else [self.renderable] + width = ( + measure_renderables(console, renderables, max_width - padding - 2).maximum + + padding + + 2 + ) return Measurement(width, width) diff --git a/rich/pretty.py b/rich/pretty.py index 5248fd9b6..d9289006f 100644 --- a/rich/pretty.py +++ b/rich/pretty.py @@ -108,6 +108,7 @@ def __rich_console__( justify=self.justify or options.justify, overflow=self.overflow or options.overflow, no_wrap=pick_bool(self.no_wrap, options.no_wrap), + style="pretty", ) pretty_text = self.highlighter(pretty_text) yield pretty_text diff --git a/rich/progress.py b/rich/progress.py index 66fb35aff..947f482ce 100644 --- a/rich/progress.py +++ b/rich/progress.py @@ -927,7 +927,7 @@ def process_renderables( syntax = Syntax( '''def loop_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]: - """Iterate and generate a tup`le with a flag for last value.""" + """Iterate and generate a tuple with a flag for last value.""" iter_values = iter(values) try: previous_value = next(iter_values) diff --git a/rich/style.py b/rich/style.py index a0e28c3e8..b51393b58 100644 --- a/rich/style.py +++ b/rich/style.py @@ -118,23 +118,6 @@ def _make_color(color: Union[Color, str]) -> Color: self._color = None if color is None else _make_color(color) self._bgcolor = None if bgcolor is None else _make_color(bgcolor) - self._attributes = sum( - ( - bold and 1 or 0, - dim and 2 or 0, - italic and 4 or 0, - underline and 8 or 0, - blink and 16 or 0, - blink2 and 32 or 0, - reverse and 64 or 0, - conceal and 128 or 0, - strike and 256 or 0, - underline2 and 512 or 0, - frame and 1024 or 0, - encircle and 2048 or 0, - overline and 4096 or 0, - ) - ) self._set_attributes = sum( ( bold is not None, @@ -152,6 +135,28 @@ def _make_color(color: Union[Color, str]) -> Color: overline is not None and 4096, ) ) + self._attributes = ( + sum( + ( + bold and 1 or 0, + dim and 2 or 0, + italic and 4 or 0, + underline and 8 or 0, + blink and 16 or 0, + blink2 and 32 or 0, + reverse and 64 or 0, + conceal and 128 or 0, + strike and 256 or 0, + underline2 and 512 or 0, + frame and 1024 or 0, + encircle and 2048 or 0, + overline and 4096 or 0, + ) + ) + if self._set_attributes + else 0 + ) + self._link = link self._link_id = f"{time()}-{randint(0, 999999)}" if link else "" self._hash = hash( diff --git a/rich/syntax.py b/rich/syntax.py index 2a86c3eac..48940209f 100644 --- a/rich/syntax.py +++ b/rich/syntax.py @@ -104,12 +104,12 @@ class SyntaxTheme(ABC): @abstractmethod def get_style_for_token(self, token_type: TokenType) -> Style: """Get a style for a given Pygments token.""" - raise NotImplementedError + raise NotImplementedError # pragma: no cover @abstractmethod def get_background_style(self) -> Style: """Get the background color.""" - raise NotImplementedError + raise NotImplementedError # pragma: no cover class PygmentsSyntaxTheme(SyntaxTheme): @@ -302,8 +302,7 @@ def from_path( if lexer is None: try: - lexer = guess_lexer_for_filename(path, code) - lexer_name = lexer.name + lexer_name = guess_lexer_for_filename(path, code).name except ClassNotFound: pass @@ -494,7 +493,7 @@ def __rich_console__( if first: line_column = str(line_no).rjust(numbers_column_width - 2) + " " if highlight_line(line_no): - yield _Segment(line_pointer, number_style) + yield _Segment(line_pointer, Style(color="red")) yield _Segment(line_column, highlight_number_style) else: yield _Segment(" ", highlight_number_style) diff --git a/rich/table.py b/rich/table.py index efb72eb75..a0438d2f3 100644 --- a/rich/table.py +++ b/rich/table.py @@ -241,19 +241,23 @@ def __rich_measure__(self, console: "Console", max_width: int) -> Measurement: if self.width is not None: max_width = self.width - if self.box: - max_width -= len(self.columns) - 1 - if self.show_edge: - max_width -= 2 + # if self.box: + # max_width -= len(self.columns) - 1 + # if self.show_edge: + # max_width -= 2 if max_width < 0: return Measurement(0, 0) extra_width = self._extra_width + + max_width = sum(self._calculate_column_widths(console, max_width)) + _measure_column = self._measure_column measurements = [ - _measure_column(console, column, max_width) for column in self.columns + _measure_column(console, column, max_width - extra_width) + for column in self.columns ] minimum_width = ( sum(measurement.minimum for measurement in measurements) + extra_width diff --git a/rich/theme.py b/rich/theme.py index c8dccf269..7f161d9c1 100644 --- a/rich/theme.py +++ b/rich/theme.py @@ -91,10 +91,9 @@ def push_theme(self, theme: Theme, inherit: bool = True) -> None: 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() + styles = ( + {**self._entries[-1], **theme.styles} if inherit else theme.styles.copy() + ) self._entries.append(styles) self.get = self._entries[-1].get diff --git a/rich/traceback.py b/rich/traceback.py index 215ef5330..37d4015c6 100644 --- a/rich/traceback.py +++ b/rich/traceback.py @@ -3,13 +3,24 @@ import platform import sys from dataclasses import dataclass, field -from traceback import extract_tb, walk_tb, StackSummary +from traceback import walk_tb from types import TracebackType -from typing import Any, Callable, Dict, List, Optional, Type +from typing import Callable, Dict, List, Optional, Type from pygments.lexers import guess_lexer_for_filename -from pygments.token import Token, String, Name, Number +from pygments.token import ( + Comment, + Generic, + Keyword, + Name, + Number, + Operator, + String, + Token, + Text as TextToken, +) +from . import pretty from ._loop import loop_first, loop_last from .columns import Columns from .console import ( @@ -21,13 +32,12 @@ ) from .constrain import Constrain from .highlighter import RegexHighlighter, ReprHighlighter -from .padding import Padding from .panel import Panel -from . import pretty from .scope import render_scope -from .syntax import Syntax from .style import Style +from .syntax import Syntax from .text import Text +from .theme import Theme WINDOWS = platform.system() == "Windows" @@ -262,18 +272,33 @@ def __rich_console__( ) -> RenderResult: theme = self.theme background_style = theme.get_background_style() + token_style = theme.get_style_for_token + + traceback_theme = Theme( + { + "pretty": token_style(TextToken), + "pygments.text": token_style(Token), + "pygments.string": token_style(String), + "pygments.function": token_style(Name.Function), + "pygments.number": token_style(Number), + "repr.str": token_style(String), + "repr.brace": token_style(TextToken) + Style(bold=True), + "repr.number": token_style(Number), + "repr.bool_true": token_style(Keyword.Constant), + "repr.bool_false": token_style(Keyword.Constant), + "repr.none": token_style(Keyword.Constant), + "scope.border": token_style(String.Delimiter), + "scope.equals": token_style(Operator), + "scope.key": token_style(Name), + "scope.key.special": token_style(Name.Constant) + Style(dim=True), + } + ) - styles = { - "text": theme.get_style_for_token(Token), - "string": theme.get_style_for_token(String), - "function": theme.get_style_for_token(Name.Function), - "number": theme.get_style_for_token(Number), - } highlighter = ReprHighlighter() for last, stack in loop_last(reversed(self.trace.stacks)): if stack.frames: stack_renderable: ConsoleRenderable = Panel( - self._render_stack(stack, styles), + self._render_stack(stack), title="[traceback.title]Traceback [dim](most recent call last)", style=background_style, border_style="traceback.border.syntax_error", @@ -281,18 +306,20 @@ def __rich_console__( padding=(0, 1), ) stack_renderable = Constrain(stack_renderable, self.width) - yield stack_renderable + with console.use_theme(traceback_theme): + yield stack_renderable if stack.syntax_error is not None: - yield Constrain( - Panel( - self._render_syntax_error(stack.syntax_error, styles), - style=background_style, - border_style="traceback.border", - expand=False, - padding=(0, 1), - ), - self.width, - ) + with console.use_theme(traceback_theme): + yield Constrain( + Panel( + self._render_syntax_error(stack.syntax_error), + style=background_style, + border_style="traceback.border", + expand=False, + padding=(0, 1), + ), + self.width, + ) yield Text.assemble( (f"{stack.exc_type}: ", "traceback.exc_type"), highlighter(stack.syntax_error.msg), @@ -309,17 +336,15 @@ def __rich_console__( ) @render_group() - def _render_syntax_error( - self, syntax_error: _SyntaxError, styles: Dict[str, Style] - ) -> RenderResult: + def _render_syntax_error(self, syntax_error: _SyntaxError) -> RenderResult: highlighter = ReprHighlighter() path_highlighter = PathHighlighter() if syntax_error.filename != "": text = Text.assemble( - (f" {syntax_error.filename}", styles["string"]), - (":", styles["text"]), - (str(syntax_error.lineno), styles["number"]), - style=styles["text"], + (f" {syntax_error.filename}", "pygments.string"), + (":", "pgments.text"), + (str(syntax_error.lineno), "pygments.number"), + style="pygments.text", ) yield path_highlighter(text) syntax_error_text = highlighter(syntax_error.line.rstrip()) @@ -328,12 +353,12 @@ def _render_syntax_error( syntax_error_text.stylize("bold underline", offset, offset + 1) syntax_error_text += Text.from_markup( "\n" + " " * offset + "[traceback.offset]▲[/]", - style=styles["text"], + style="pygments.text", ) yield syntax_error_text @render_group() - def _render_stack(self, stack: Stack, styles: Dict[str, Style]) -> RenderResult: + def _render_stack(self, stack: Stack) -> RenderResult: path_highlighter = PathHighlighter() theme = self.theme code_cache: Dict[str, str] = {} @@ -356,12 +381,12 @@ def read_code(filename: str) -> str: for first, frame in loop_first(stack.frames): text = Text.assemble( - path_highlighter(Text(frame.filename, style=styles["string"])), - (":", styles["text"]), - (str(frame.lineno), styles["number"]), + path_highlighter(Text(frame.filename, style="pygments.string")), + (":", "pygments.text"), + (str(frame.lineno), "pygments.number"), " in ", - (frame.name, styles["function"]), - style=styles["text"], + (frame.name, "pygments.function"), + style="pygments.text", ) if not frame.filename.startswith("<") and not first: yield "" @@ -414,7 +439,15 @@ def bar(a): # 这是对亚洲语言支持的测试。面对模棱两可的想 print(one / a) def foo(a): - zed = {"list_of_things": ["foo", "bar", "baz"] * 3} + zed = { + "characters": { + "Paul Atriedies", + "Vladimir Harkonnen", + "Thufir Haway", + "Duncan Idaho", + }, + "atomic_types": (None, False, True), + } bar(a) def error(): @@ -425,8 +458,6 @@ def error(): except: slfkjsldkfj # type: ignore except: - tb = Traceback(show_locals=True, theme="monokai") - # print(fooads) - console.print(tb) + console.print_exception(show_locals=True) - error() \ No newline at end of file + error() diff --git a/tests/test_log.py b/tests/test_log.py index edb3fee20..d5fb49531 100644 --- a/tests/test_log.py +++ b/tests/test_log.py @@ -27,8 +27,10 @@ def render_log(): def test_log(): - expected = "\n\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mHello from \x1b[1m<\x1b[0m\x1b[1;95mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[3;33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m ! \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:24\x1b[0m\n\x1b[2;36m \x1b[0m\x1b[2;36m \x1b[0m\x1b[1m[\x1b[0m\x1b[1;34m1\x1b[0m, \x1b[1;34m2\x1b[0m, \x1b[1;34m3\x1b[0m\x1b[1m]\x1b[0m \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:25\x1b[0m\n \x1b[34m╭─\x1b[0m\x1b[34m───────────────────── \x1b[0m\x1b[3;34mlocals\x1b[0m\x1b[34m ─────────────────────\x1b[0m\x1b[34m─╮\x1b[0m \n \x1b[34m│\x1b[0m \x1b[3;33mconsole\x1b[0m\x1b[31m =\x1b[0m \x1b[1m<\x1b[0m\x1b[1;95mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[3;33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m \x1b[34m│\x1b[0m \n \x1b[34m╰────────────────────────────────────────────────────╯\x1b[0m \n" - assert render_log() == expected + expected = "\n\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0mHello from \x1b[1m<\x1b[0m\x1b[1;95mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[3;33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;34m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m ! \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:24\x1b[0m\n\x1b[2;36m \x1b[0m\x1b[2;36m \x1b[0m\x1b[1m[\x1b[0m\x1b[1;34m1\x1b[0m, \x1b[1;34m2\x1b[0m, \x1b[1;34m3\x1b[0m\x1b[1m]\x1b[0m \x1b]8;id=0;foo\x1b\\\x1b[2mtest_log.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:25\x1b[0m\n \x1b[34m╭─\x1b[0m\x1b[34m───────────────────── \x1b[0m\x1b[3;34mlocals\x1b[0m\x1b[34m ─────────────────────\x1b[0m\x1b[34m─╮\x1b[0m \n \x1b[34m│\x1b[0m \x1b[3;33;44mconsole\x1b[0m\x1b[31;44m =\x1b[0m\x1b[44m \x1b[0m\x1b[1;44m<\x1b[0m\x1b[1;95;44mconsole\x1b[0m\x1b[39;44m \x1b[0m\x1b[3;33;44mwidth\x1b[0m\x1b[39;44m=\x1b[0m\x1b[1;34;44m80\x1b[0m\x1b[39;44m ColorSystem.TRUECOLOR\x1b[0m\x1b[1;44m>\x1b[0m \x1b[34m│\x1b[0m \n \x1b[34m╰────────────────────────────────────────────────────╯\x1b[0m \n" + rendered = render_log() + print(repr(rendered)) + assert rendered == expected if __name__ == "__main__": diff --git a/tests/test_syntax.py b/tests/test_syntax.py index 3e5a4bd33..7070669a7 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -16,6 +16,8 @@ def loop_first_last(values: Iterable[T]) -> Iterable[Tuple[bool, bool, T]]: yield first, True, previous_value ''' +import os, tempfile + from .render import render from rich.panel import Panel @@ -48,6 +50,28 @@ def test_ansi_theme(): assert theme.get_background_style() == Style() +def test_from_file(): + fh, path = tempfile.mkstemp("example.py") + try: + os.write(fh, b"import this\n") + syntax = Syntax.from_path(path) + assert syntax.lexer_name == "Python" + assert syntax.code == "import this\n" + finally: + os.remove(path) + + +def test_from_file_unknown_lexer(): + fh, path = tempfile.mkstemp("example.nosuchtype") + try: + os.write(fh, b"import this\n") + syntax = Syntax.from_path(path) + assert syntax.lexer_name == "default" + assert syntax.code == "import this\n" + finally: + os.remove(path) + + if __name__ == "__main__": syntax = Panel.fit( Syntax( diff --git a/tests/test_theme.py b/tests/test_theme.py index df426e512..96390894b 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -2,8 +2,10 @@ import os import tempfile +import pytest + from rich.style import Style -from rich.theme import Theme +from rich.theme import Theme, ThemeStack, ThemeStackError def test_inherit(): @@ -36,3 +38,16 @@ def test_read(): write_theme.write(theme.config) load_theme = Theme.read(filename) assert theme.styles == load_theme.styles + + +def test_theme_stack(): + theme = Theme({"warning": "red"}) + stack = ThemeStack(theme) + assert stack.get("warning") == Style.parse("red") + new_theme = Theme({"warning": "bold yellow"}) + stack.push_theme(new_theme) + assert stack.get("warning") == Style.parse("bold yellow") + stack.pop_theme() + assert stack.get("warning") == Style.parse("red") + with pytest.raises(ThemeStackError): + stack.pop_theme() \ No newline at end of file