diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 1551e3657..5aaf87765 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -25,3 +25,4 @@ The following people have contributed to the development of Rich: - [Tim Savage](https://github.com/timsavage) - [Nicolas Simonds](https://github.com/0xDEC0DE) - [Gabriele N. Tornetta](https://github.com/p403n1x87) +- [Patrick Arminio](https://github.com/patrick91) diff --git a/rich/syntax.py b/rich/syntax.py index 05d41c993..759351907 100644 --- a/rich/syntax.py +++ b/rich/syntax.py @@ -5,6 +5,7 @@ from abc import ABC, abstractmethod from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Type, Union +from pygments.lexer import Lexer from pygments.lexers import get_lexer_by_name, guess_lexer_for_filename from pygments.style import Style as PygmentsStyle from pygments.styles import get_style_by_name @@ -194,7 +195,7 @@ class Syntax(JupyterMixin): Args: code (str): Code to highlight. - lexer_name (str): Lexer to use (see https://pygments.org/docs/lexers/) + lexer (Lexer | str): Lexer to use (see https://pygments.org/docs/lexers/) theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "monokai". dedent (bool, optional): Enable stripping of initial whitespace. Defaults to False. line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False. @@ -226,7 +227,7 @@ def get_theme(cls, name: Union[str, SyntaxTheme]) -> SyntaxTheme: def __init__( self, code: str, - lexer_name: str, + lexer: Union[Lexer, str], *, theme: Union[str, SyntaxTheme] = DEFAULT_THEME, dedent: bool = False, @@ -241,7 +242,7 @@ def __init__( indent_guides: bool = False, ) -> None: self.code = code - self.lexer_name = lexer_name + self._lexer = lexer self.dedent = dedent self.line_numbers = line_numbers self.start_line = start_line @@ -348,6 +349,25 @@ def _get_token_color(self, token_type: TokenType) -> Optional[Color]: style = self._theme.get_style_for_token(token_type) return style.color + @property + def lexer(self) -> Optional[Lexer]: + """The lexer for this syntax, or None if no lexer was found. + + Tries to find the lexer by name if a string was passed to the constructor. + """ + + if isinstance(self._lexer, Lexer): + return self._lexer + try: + return get_lexer_by_name( + self._lexer, + stripnl=False, + ensurenl=True, + tabsize=self.tab_size, + ) + except ClassNotFound: + return None + def highlight( self, code: str, line_range: Optional[Tuple[int, int]] = None ) -> Text: @@ -373,14 +393,10 @@ def highlight( no_wrap=not self.word_wrap, ) _get_theme_style = self._theme.get_style_for_token - try: - lexer = get_lexer_by_name( - self.lexer_name, - stripnl=False, - ensurenl=True, - tabsize=self.tab_size, - ) - except ClassNotFound: + + lexer = self.lexer + + if lexer is None: text.append(code) else: if line_range: @@ -390,6 +406,8 @@ def highlight( def line_tokenize() -> Iterable[Tuple[Any, str]]: """Split tokens to one per line.""" + assert lexer + for token_type, token in lexer.get_tokens(code): while token: line_token, new_line, token = token.partition("\n") @@ -698,7 +716,7 @@ def __rich_console__( code = sys.stdin.read() syntax = Syntax( code=code, - lexer_name=args.lexer_name, + lexer=args.lexer_name, line_numbers=args.line_numbers, word_wrap=args.word_wrap, theme=args.theme, diff --git a/tests/test_syntax.py b/tests/test_syntax.py index 090d08d25..de739c8d8 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -10,6 +10,8 @@ from rich.style import Style from rich.syntax import Syntax, ANSISyntaxTheme, PygmentsSyntaxTheme, Color, Console +from pygments.lexers import PythonLexer + CODE = '''\ def loop_first_last(values: Iterable[T]) -> Iterable[Tuple[bool, bool, T]]: @@ -30,7 +32,7 @@ def loop_first_last(values: Iterable[T]) -> Iterable[Tuple[bool, bool, T]]: def test_blank_lines(): code = "\n\nimport this\n\n" syntax = Syntax( - code, lexer_name="python", theme="ascii_light", code_width=30, line_numbers=True + code, lexer="python", theme="ascii_light", code_width=30, line_numbers=True ) result = render(syntax) print(repr(result)) @@ -44,7 +46,7 @@ def test_python_render(): syntax = Panel.fit( Syntax( CODE, - lexer_name="python", + lexer="python", line_numbers=True, line_range=(2, 10), theme="foo", @@ -62,7 +64,22 @@ def test_python_render(): def test_python_render_simple(): syntax = Syntax( CODE, - lexer_name="python", + lexer="python", + line_numbers=False, + theme="foo", + code_width=60, + word_wrap=False, + ) + rendered_syntax = render(syntax) + print(repr(rendered_syntax)) + expected = '\x1b[1;38;2;0;128;0;48;2;248;248;248mdef\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;255;48;2;248;248;248mloop_first_last\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m(\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mvalues\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m:\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mIterable\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m[\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mT\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m]\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m)\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;102;102;102;48;2;248;248;248m-\x1b[0m\x1b[38;2;102;102;102;48;2;248;248;248m>\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mIterable\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m[\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mTuple\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m[\x1b[0m\x1b[38;2;0;128;0;48;2;248;248;248mb\x1b[0m\n\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[3;38;2;186;33;33;48;2;248;248;248m"""Iterate and generate a tuple with a flag for first an\x1b[0m\n\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248miter_values\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;102;102;102;48;2;248;248;248m=\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;128;0;48;2;248;248;248miter\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m(\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mvalues\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m)\x1b[0m\x1b[48;2;248;248;248m \x1b[0m\n\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mtry\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m:\x1b[0m\x1b[48;2;248;248;248m \x1b[0m\n\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mprevious_value\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;102;102;102;48;2;248;248;248m=\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;128;0;48;2;248;248;248mnext\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m(\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248miter_values\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m)\x1b[0m\x1b[48;2;248;248;248m \x1b[0m\n\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mexcept\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;210;65;58;48;2;248;248;248mStopIteration\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m:\x1b[0m\x1b[48;2;248;248;248m \x1b[0m\n\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mreturn\x1b[0m\x1b[48;2;248;248;248m \x1b[0m\n\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mfirst\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;102;102;102;48;2;248;248;248m=\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mTrue\x1b[0m\x1b[48;2;248;248;248m \x1b[0m\n\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mfor\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mvalue\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;170;34;255;48;2;248;248;248min\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248miter_values\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m:\x1b[0m\x1b[48;2;248;248;248m \x1b[0m\n\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248myield\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mfirst\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m,\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mFalse\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m,\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mprevious_value\x1b[0m\x1b[48;2;248;248;248m \x1b[0m\n\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mfirst\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;102;102;102;48;2;248;248;248m=\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mFalse\x1b[0m\x1b[48;2;248;248;248m \x1b[0m\n\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mprevious_value\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;102;102;102;48;2;248;248;248m=\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mvalue\x1b[0m\x1b[48;2;248;248;248m \x1b[0m\n\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248myield\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mfirst\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m,\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[1;38;2;0;128;0;48;2;248;248;248mTrue\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m,\x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248m \x1b[0m\x1b[38;2;0;0;0;48;2;248;248;248mprevious_value\x1b[0m\x1b[48;2;248;248;248m \x1b[0m\n' + assert rendered_syntax == expected + + +def test_python_render_simple_passing_lexer_instance(): + syntax = Syntax( + CODE, + lexer=PythonLexer(), line_numbers=False, theme="foo", code_width=60, @@ -77,7 +94,7 @@ def test_python_render_simple(): def test_python_render_simple_indent_guides(): syntax = Syntax( CODE, - lexer_name="python", + lexer="python", line_numbers=False, theme="ansi_light", code_width=60, @@ -93,7 +110,7 @@ def test_python_render_simple_indent_guides(): def test_python_render_line_range_indent_guides(): syntax = Syntax( CODE, - lexer_name="python", + lexer="python", line_numbers=False, theme="ansi_light", code_width=60, @@ -111,7 +128,7 @@ def test_python_render_indent_guides(): syntax = Panel.fit( Syntax( CODE, - lexer_name="python", + lexer="python", line_numbers=True, line_range=(2, 10), theme="foo", @@ -144,7 +161,7 @@ def test_get_line_color_none(): style._background_style = Style(bgcolor=None) syntax = Syntax( CODE, - lexer_name="python", + lexer="python", line_numbers=True, line_range=(2, 10), theme=style, @@ -158,7 +175,7 @@ def test_get_line_color_none(): def test_highlight_background_color(): syntax = Syntax( CODE, - lexer_name="python", + lexer="python", line_numbers=True, line_range=(2, 10), theme="foo", @@ -189,7 +206,7 @@ def test_get_style_for_token(): style._style_cache = style_dict syntax = Syntax( CODE, - lexer_name="python", + lexer="python", line_numbers=True, line_range=(2, 10), theme=style, @@ -203,7 +220,7 @@ def test_get_style_for_token(): def test_option_no_wrap(): syntax = Syntax( CODE, - lexer_name="python", + lexer="python", line_numbers=True, line_range=(2, 10), code_width=60, @@ -230,7 +247,8 @@ def test_from_file(): try: os.write(fh, b"import this\n") syntax = Syntax.from_path(path) - assert syntax.lexer_name == "Python" + assert syntax.lexer + assert syntax.lexer.name == "Python" assert syntax.code == "import this\n" finally: os.remove(path) @@ -242,7 +260,7 @@ def test_from_file_unknown_lexer(): try: os.write(fh, b"import this\n") syntax = Syntax.from_path(path) - assert syntax.lexer_name == "default" + assert syntax.lexer is None assert syntax.code == "import this\n" finally: os.remove(path) @@ -252,7 +270,7 @@ def test_from_file_unknown_lexer(): syntax = Panel.fit( Syntax( CODE, - lexer_name="python", + lexer="python", line_numbers=True, line_range=(2, 10), theme="foo",