Skip to content

Commit

Permalink
Merge pull request #1748 from patrick91/feature/allow-to-pass-lexer-i…
Browse files Browse the repository at this point in the history
…nstance

Add lexer parameter to Syntax
  • Loading branch information
willmcgugan committed Jan 5, 2022
2 parents 5ca63d7 + de7ed16 commit 22989e4
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 25 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Expand Up @@ -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)
42 changes: 30 additions & 12 deletions rich/syntax.py
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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")
Expand Down Expand Up @@ -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,
Expand Down
44 changes: 31 additions & 13 deletions tests/test_syntax.py
Expand Up @@ -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]]:
Expand All @@ -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))
Expand All @@ -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",
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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",
Expand Down Expand Up @@ -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,
Expand All @@ -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",
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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",
Expand Down

0 comments on commit 22989e4

Please sign in to comment.