Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pip/_vendor/rich/syntax.py: 23%
329 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:48 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:48 +0000
1import os.path
2import platform
3import re
4import sys
5import textwrap
6from abc import ABC, abstractmethod
7from pathlib import Path
8from typing import (
9 Any,
10 Dict,
11 Iterable,
12 List,
13 NamedTuple,
14 Optional,
15 Sequence,
16 Set,
17 Tuple,
18 Type,
19 Union,
20)
22from pip._vendor.pygments.lexer import Lexer
23from pip._vendor.pygments.lexers import get_lexer_by_name, guess_lexer_for_filename
24from pip._vendor.pygments.style import Style as PygmentsStyle
25from pip._vendor.pygments.styles import get_style_by_name
26from pip._vendor.pygments.token import (
27 Comment,
28 Error,
29 Generic,
30 Keyword,
31 Name,
32 Number,
33 Operator,
34 String,
35 Token,
36 Whitespace,
37)
38from pip._vendor.pygments.util import ClassNotFound
40from pip._vendor.rich.containers import Lines
41from pip._vendor.rich.padding import Padding, PaddingDimensions
43from ._loop import loop_first
44from .cells import cell_len
45from .color import Color, blend_rgb
46from .console import Console, ConsoleOptions, JustifyMethod, RenderResult
47from .jupyter import JupyterMixin
48from .measure import Measurement
49from .segment import Segment, Segments
50from .style import Style, StyleType
51from .text import Text
53TokenType = Tuple[str, ...]
55WINDOWS = platform.system() == "Windows"
56DEFAULT_THEME = "monokai"
58# The following styles are based on https://github.com/pygments/pygments/blob/master/pygments/formatters/terminal.py
59# A few modifications were made
61ANSI_LIGHT: Dict[TokenType, Style] = {
62 Token: Style(),
63 Whitespace: Style(color="white"),
64 Comment: Style(dim=True),
65 Comment.Preproc: Style(color="cyan"),
66 Keyword: Style(color="blue"),
67 Keyword.Type: Style(color="cyan"),
68 Operator.Word: Style(color="magenta"),
69 Name.Builtin: Style(color="cyan"),
70 Name.Function: Style(color="green"),
71 Name.Namespace: Style(color="cyan", underline=True),
72 Name.Class: Style(color="green", underline=True),
73 Name.Exception: Style(color="cyan"),
74 Name.Decorator: Style(color="magenta", bold=True),
75 Name.Variable: Style(color="red"),
76 Name.Constant: Style(color="red"),
77 Name.Attribute: Style(color="cyan"),
78 Name.Tag: Style(color="bright_blue"),
79 String: Style(color="yellow"),
80 Number: Style(color="blue"),
81 Generic.Deleted: Style(color="bright_red"),
82 Generic.Inserted: Style(color="green"),
83 Generic.Heading: Style(bold=True),
84 Generic.Subheading: Style(color="magenta", bold=True),
85 Generic.Prompt: Style(bold=True),
86 Generic.Error: Style(color="bright_red"),
87 Error: Style(color="red", underline=True),
88}
90ANSI_DARK: Dict[TokenType, Style] = {
91 Token: Style(),
92 Whitespace: Style(color="bright_black"),
93 Comment: Style(dim=True),
94 Comment.Preproc: Style(color="bright_cyan"),
95 Keyword: Style(color="bright_blue"),
96 Keyword.Type: Style(color="bright_cyan"),
97 Operator.Word: Style(color="bright_magenta"),
98 Name.Builtin: Style(color="bright_cyan"),
99 Name.Function: Style(color="bright_green"),
100 Name.Namespace: Style(color="bright_cyan", underline=True),
101 Name.Class: Style(color="bright_green", underline=True),
102 Name.Exception: Style(color="bright_cyan"),
103 Name.Decorator: Style(color="bright_magenta", bold=True),
104 Name.Variable: Style(color="bright_red"),
105 Name.Constant: Style(color="bright_red"),
106 Name.Attribute: Style(color="bright_cyan"),
107 Name.Tag: Style(color="bright_blue"),
108 String: Style(color="yellow"),
109 Number: Style(color="bright_blue"),
110 Generic.Deleted: Style(color="bright_red"),
111 Generic.Inserted: Style(color="bright_green"),
112 Generic.Heading: Style(bold=True),
113 Generic.Subheading: Style(color="bright_magenta", bold=True),
114 Generic.Prompt: Style(bold=True),
115 Generic.Error: Style(color="bright_red"),
116 Error: Style(color="red", underline=True),
117}
119RICH_SYNTAX_THEMES = {"ansi_light": ANSI_LIGHT, "ansi_dark": ANSI_DARK}
120NUMBERS_COLUMN_DEFAULT_PADDING = 2
123class SyntaxTheme(ABC):
124 """Base class for a syntax theme."""
126 @abstractmethod
127 def get_style_for_token(self, token_type: TokenType) -> Style:
128 """Get a style for a given Pygments token."""
129 raise NotImplementedError # pragma: no cover
131 @abstractmethod
132 def get_background_style(self) -> Style:
133 """Get the background color."""
134 raise NotImplementedError # pragma: no cover
137class PygmentsSyntaxTheme(SyntaxTheme):
138 """Syntax theme that delegates to Pygments theme."""
140 def __init__(self, theme: Union[str, Type[PygmentsStyle]]) -> None:
141 self._style_cache: Dict[TokenType, Style] = {}
142 if isinstance(theme, str):
143 try:
144 self._pygments_style_class = get_style_by_name(theme)
145 except ClassNotFound:
146 self._pygments_style_class = get_style_by_name("default")
147 else:
148 self._pygments_style_class = theme
150 self._background_color = self._pygments_style_class.background_color
151 self._background_style = Style(bgcolor=self._background_color)
153 def get_style_for_token(self, token_type: TokenType) -> Style:
154 """Get a style from a Pygments class."""
155 try:
156 return self._style_cache[token_type]
157 except KeyError:
158 try:
159 pygments_style = self._pygments_style_class.style_for_token(token_type)
160 except KeyError:
161 style = Style.null()
162 else:
163 color = pygments_style["color"]
164 bgcolor = pygments_style["bgcolor"]
165 style = Style(
166 color="#" + color if color else "#000000",
167 bgcolor="#" + bgcolor if bgcolor else self._background_color,
168 bold=pygments_style["bold"],
169 italic=pygments_style["italic"],
170 underline=pygments_style["underline"],
171 )
172 self._style_cache[token_type] = style
173 return style
175 def get_background_style(self) -> Style:
176 return self._background_style
179class ANSISyntaxTheme(SyntaxTheme):
180 """Syntax theme to use standard colors."""
182 def __init__(self, style_map: Dict[TokenType, Style]) -> None:
183 self.style_map = style_map
184 self._missing_style = Style.null()
185 self._background_style = Style.null()
186 self._style_cache: Dict[TokenType, Style] = {}
188 def get_style_for_token(self, token_type: TokenType) -> Style:
189 """Look up style in the style map."""
190 try:
191 return self._style_cache[token_type]
192 except KeyError:
193 # Styles form a hierarchy
194 # We need to go from most to least specific
195 # e.g. ("foo", "bar", "baz") to ("foo", "bar") to ("foo",)
196 get_style = self.style_map.get
197 token = tuple(token_type)
198 style = self._missing_style
199 while token:
200 _style = get_style(token)
201 if _style is not None:
202 style = _style
203 break
204 token = token[:-1]
205 self._style_cache[token_type] = style
206 return style
208 def get_background_style(self) -> Style:
209 return self._background_style
212SyntaxPosition = Tuple[int, int]
215class _SyntaxHighlightRange(NamedTuple):
216 """
217 A range to highlight in a Syntax object.
218 `start` and `end` are 2-integers tuples, where the first integer is the line number
219 (starting from 1) and the second integer is the column index (starting from 0).
220 """
222 style: StyleType
223 start: SyntaxPosition
224 end: SyntaxPosition
227class Syntax(JupyterMixin):
228 """Construct a Syntax object to render syntax highlighted code.
230 Args:
231 code (str): Code to highlight.
232 lexer (Lexer | str): Lexer to use (see https://pygments.org/docs/lexers/)
233 theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "monokai".
234 dedent (bool, optional): Enable stripping of initial whitespace. Defaults to False.
235 line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False.
236 start_line (int, optional): Starting number for line numbers. Defaults to 1.
237 line_range (Tuple[int | None, int | None], optional): If given should be a tuple of the start and end line to render.
238 A value of None in the tuple indicates the range is open in that direction.
239 highlight_lines (Set[int]): A set of line numbers to highlight.
240 code_width: Width of code to render (not including line numbers), or ``None`` to use all available width.
241 tab_size (int, optional): Size of tabs. Defaults to 4.
242 word_wrap (bool, optional): Enable word wrapping.
243 background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.
244 indent_guides (bool, optional): Show indent guides. Defaults to False.
245 padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding).
246 """
248 _pygments_style_class: Type[PygmentsStyle]
249 _theme: SyntaxTheme
251 @classmethod
252 def get_theme(cls, name: Union[str, SyntaxTheme]) -> SyntaxTheme:
253 """Get a syntax theme instance."""
254 if isinstance(name, SyntaxTheme):
255 return name
256 theme: SyntaxTheme
257 if name in RICH_SYNTAX_THEMES:
258 theme = ANSISyntaxTheme(RICH_SYNTAX_THEMES[name])
259 else:
260 theme = PygmentsSyntaxTheme(name)
261 return theme
263 def __init__(
264 self,
265 code: str,
266 lexer: Union[Lexer, str],
267 *,
268 theme: Union[str, SyntaxTheme] = DEFAULT_THEME,
269 dedent: bool = False,
270 line_numbers: bool = False,
271 start_line: int = 1,
272 line_range: Optional[Tuple[Optional[int], Optional[int]]] = None,
273 highlight_lines: Optional[Set[int]] = None,
274 code_width: Optional[int] = None,
275 tab_size: int = 4,
276 word_wrap: bool = False,
277 background_color: Optional[str] = None,
278 indent_guides: bool = False,
279 padding: PaddingDimensions = 0,
280 ) -> None:
281 self.code = code
282 self._lexer = lexer
283 self.dedent = dedent
284 self.line_numbers = line_numbers
285 self.start_line = start_line
286 self.line_range = line_range
287 self.highlight_lines = highlight_lines or set()
288 self.code_width = code_width
289 self.tab_size = tab_size
290 self.word_wrap = word_wrap
291 self.background_color = background_color
292 self.background_style = (
293 Style(bgcolor=background_color) if background_color else Style()
294 )
295 self.indent_guides = indent_guides
296 self.padding = padding
298 self._theme = self.get_theme(theme)
299 self._stylized_ranges: List[_SyntaxHighlightRange] = []
301 @classmethod
302 def from_path(
303 cls,
304 path: str,
305 encoding: str = "utf-8",
306 lexer: Optional[Union[Lexer, str]] = None,
307 theme: Union[str, SyntaxTheme] = DEFAULT_THEME,
308 dedent: bool = False,
309 line_numbers: bool = False,
310 line_range: Optional[Tuple[int, int]] = None,
311 start_line: int = 1,
312 highlight_lines: Optional[Set[int]] = None,
313 code_width: Optional[int] = None,
314 tab_size: int = 4,
315 word_wrap: bool = False,
316 background_color: Optional[str] = None,
317 indent_guides: bool = False,
318 padding: PaddingDimensions = 0,
319 ) -> "Syntax":
320 """Construct a Syntax object from a file.
322 Args:
323 path (str): Path to file to highlight.
324 encoding (str): Encoding of file.
325 lexer (str | Lexer, optional): Lexer to use. If None, lexer will be auto-detected from path/file content.
326 theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "emacs".
327 dedent (bool, optional): Enable stripping of initial whitespace. Defaults to True.
328 line_numbers (bool, optional): Enable rendering of line numbers. Defaults to False.
329 start_line (int, optional): Starting number for line numbers. Defaults to 1.
330 line_range (Tuple[int, int], optional): If given should be a tuple of the start and end line to render.
331 highlight_lines (Set[int]): A set of line numbers to highlight.
332 code_width: Width of code to render (not including line numbers), or ``None`` to use all available width.
333 tab_size (int, optional): Size of tabs. Defaults to 4.
334 word_wrap (bool, optional): Enable word wrapping of code.
335 background_color (str, optional): Optional background color, or None to use theme color. Defaults to None.
336 indent_guides (bool, optional): Show indent guides. Defaults to False.
337 padding (PaddingDimensions): Padding to apply around the syntax. Defaults to 0 (no padding).
339 Returns:
340 [Syntax]: A Syntax object that may be printed to the console
341 """
342 code = Path(path).read_text(encoding=encoding)
344 if not lexer:
345 lexer = cls.guess_lexer(path, code=code)
347 return cls(
348 code,
349 lexer,
350 theme=theme,
351 dedent=dedent,
352 line_numbers=line_numbers,
353 line_range=line_range,
354 start_line=start_line,
355 highlight_lines=highlight_lines,
356 code_width=code_width,
357 tab_size=tab_size,
358 word_wrap=word_wrap,
359 background_color=background_color,
360 indent_guides=indent_guides,
361 padding=padding,
362 )
364 @classmethod
365 def guess_lexer(cls, path: str, code: Optional[str] = None) -> str:
366 """Guess the alias of the Pygments lexer to use based on a path and an optional string of code.
367 If code is supplied, it will use a combination of the code and the filename to determine the
368 best lexer to use. For example, if the file is ``index.html`` and the file contains Django
369 templating syntax, then "html+django" will be returned. If the file is ``index.html``, and no
370 templating language is used, the "html" lexer will be used. If no string of code
371 is supplied, the lexer will be chosen based on the file extension..
373 Args:
374 path (AnyStr): The path to the file containing the code you wish to know the lexer for.
375 code (str, optional): Optional string of code that will be used as a fallback if no lexer
376 is found for the supplied path.
378 Returns:
379 str: The name of the Pygments lexer that best matches the supplied path/code.
380 """
381 lexer: Optional[Lexer] = None
382 lexer_name = "default"
383 if code:
384 try:
385 lexer = guess_lexer_for_filename(path, code)
386 except ClassNotFound:
387 pass
389 if not lexer:
390 try:
391 _, ext = os.path.splitext(path)
392 if ext:
393 extension = ext.lstrip(".").lower()
394 lexer = get_lexer_by_name(extension)
395 except ClassNotFound:
396 pass
398 if lexer:
399 if lexer.aliases:
400 lexer_name = lexer.aliases[0]
401 else:
402 lexer_name = lexer.name
404 return lexer_name
406 def _get_base_style(self) -> Style:
407 """Get the base style."""
408 default_style = self._theme.get_background_style() + self.background_style
409 return default_style
411 def _get_token_color(self, token_type: TokenType) -> Optional[Color]:
412 """Get a color (if any) for the given token.
414 Args:
415 token_type (TokenType): A token type tuple from Pygments.
417 Returns:
418 Optional[Color]: Color from theme, or None for no color.
419 """
420 style = self._theme.get_style_for_token(token_type)
421 return style.color
423 @property
424 def lexer(self) -> Optional[Lexer]:
425 """The lexer for this syntax, or None if no lexer was found.
427 Tries to find the lexer by name if a string was passed to the constructor.
428 """
430 if isinstance(self._lexer, Lexer):
431 return self._lexer
432 try:
433 return get_lexer_by_name(
434 self._lexer,
435 stripnl=False,
436 ensurenl=True,
437 tabsize=self.tab_size,
438 )
439 except ClassNotFound:
440 return None
442 def highlight(
443 self,
444 code: str,
445 line_range: Optional[Tuple[Optional[int], Optional[int]]] = None,
446 ) -> Text:
447 """Highlight code and return a Text instance.
449 Args:
450 code (str): Code to highlight.
451 line_range(Tuple[int, int], optional): Optional line range to highlight.
453 Returns:
454 Text: A text instance containing highlighted syntax.
455 """
457 base_style = self._get_base_style()
458 justify: JustifyMethod = (
459 "default" if base_style.transparent_background else "left"
460 )
462 text = Text(
463 justify=justify,
464 style=base_style,
465 tab_size=self.tab_size,
466 no_wrap=not self.word_wrap,
467 )
468 _get_theme_style = self._theme.get_style_for_token
470 lexer = self.lexer
472 if lexer is None:
473 text.append(code)
474 else:
475 if line_range:
476 # More complicated path to only stylize a portion of the code
477 # This speeds up further operations as there are less spans to process
478 line_start, line_end = line_range
480 def line_tokenize() -> Iterable[Tuple[Any, str]]:
481 """Split tokens to one per line."""
482 assert lexer # required to make MyPy happy - we know lexer is not None at this point
484 for token_type, token in lexer.get_tokens(code):
485 while token:
486 line_token, new_line, token = token.partition("\n")
487 yield token_type, line_token + new_line
489 def tokens_to_spans() -> Iterable[Tuple[str, Optional[Style]]]:
490 """Convert tokens to spans."""
491 tokens = iter(line_tokenize())
492 line_no = 0
493 _line_start = line_start - 1 if line_start else 0
495 # Skip over tokens until line start
496 while line_no < _line_start:
497 try:
498 _token_type, token = next(tokens)
499 except StopIteration:
500 break
501 yield (token, None)
502 if token.endswith("\n"):
503 line_no += 1
504 # Generate spans until line end
505 for token_type, token in tokens:
506 yield (token, _get_theme_style(token_type))
507 if token.endswith("\n"):
508 line_no += 1
509 if line_end and line_no >= line_end:
510 break
512 text.append_tokens(tokens_to_spans())
514 else:
515 text.append_tokens(
516 (token, _get_theme_style(token_type))
517 for token_type, token in lexer.get_tokens(code)
518 )
519 if self.background_color is not None:
520 text.stylize(f"on {self.background_color}")
522 if self._stylized_ranges:
523 self._apply_stylized_ranges(text)
525 return text
527 def stylize_range(
528 self, style: StyleType, start: SyntaxPosition, end: SyntaxPosition
529 ) -> None:
530 """
531 Adds a custom style on a part of the code, that will be applied to the syntax display when it's rendered.
532 Line numbers are 1-based, while column indexes are 0-based.
534 Args:
535 style (StyleType): The style to apply.
536 start (Tuple[int, int]): The start of the range, in the form `[line number, column index]`.
537 end (Tuple[int, int]): The end of the range, in the form `[line number, column index]`.
538 """
539 self._stylized_ranges.append(_SyntaxHighlightRange(style, start, end))
541 def _get_line_numbers_color(self, blend: float = 0.3) -> Color:
542 background_style = self._theme.get_background_style() + self.background_style
543 background_color = background_style.bgcolor
544 if background_color is None or background_color.is_system_defined:
545 return Color.default()
546 foreground_color = self._get_token_color(Token.Text)
547 if foreground_color is None or foreground_color.is_system_defined:
548 return foreground_color or Color.default()
549 new_color = blend_rgb(
550 background_color.get_truecolor(),
551 foreground_color.get_truecolor(),
552 cross_fade=blend,
553 )
554 return Color.from_triplet(new_color)
556 @property
557 def _numbers_column_width(self) -> int:
558 """Get the number of characters used to render the numbers column."""
559 column_width = 0
560 if self.line_numbers:
561 column_width = (
562 len(str(self.start_line + self.code.count("\n")))
563 + NUMBERS_COLUMN_DEFAULT_PADDING
564 )
565 return column_width
567 def _get_number_styles(self, console: Console) -> Tuple[Style, Style, Style]:
568 """Get background, number, and highlight styles for line numbers."""
569 background_style = self._get_base_style()
570 if background_style.transparent_background:
571 return Style.null(), Style(dim=True), Style.null()
572 if console.color_system in ("256", "truecolor"):
573 number_style = Style.chain(
574 background_style,
575 self._theme.get_style_for_token(Token.Text),
576 Style(color=self._get_line_numbers_color()),
577 self.background_style,
578 )
579 highlight_number_style = Style.chain(
580 background_style,
581 self._theme.get_style_for_token(Token.Text),
582 Style(bold=True, color=self._get_line_numbers_color(0.9)),
583 self.background_style,
584 )
585 else:
586 number_style = background_style + Style(dim=True)
587 highlight_number_style = background_style + Style(dim=False)
588 return background_style, number_style, highlight_number_style
590 def __rich_measure__(
591 self, console: "Console", options: "ConsoleOptions"
592 ) -> "Measurement":
594 _, right, _, left = Padding.unpack(self.padding)
595 padding = left + right
596 if self.code_width is not None:
597 width = self.code_width + self._numbers_column_width + padding + 1
598 return Measurement(self._numbers_column_width, width)
599 lines = self.code.splitlines()
600 width = (
601 self._numbers_column_width
602 + padding
603 + (max(cell_len(line) for line in lines) if lines else 0)
604 )
605 if self.line_numbers:
606 width += 1
607 return Measurement(self._numbers_column_width, width)
609 def __rich_console__(
610 self, console: Console, options: ConsoleOptions
611 ) -> RenderResult:
612 segments = Segments(self._get_syntax(console, options))
613 if self.padding:
614 yield Padding(
615 segments, style=self._theme.get_background_style(), pad=self.padding
616 )
617 else:
618 yield segments
620 def _get_syntax(
621 self,
622 console: Console,
623 options: ConsoleOptions,
624 ) -> Iterable[Segment]:
625 """
626 Get the Segments for the Syntax object, excluding any vertical/horizontal padding
627 """
628 transparent_background = self._get_base_style().transparent_background
629 code_width = (
630 (
631 (options.max_width - self._numbers_column_width - 1)
632 if self.line_numbers
633 else options.max_width
634 )
635 if self.code_width is None
636 else self.code_width
637 )
639 ends_on_nl, processed_code = self._process_code(self.code)
640 text = self.highlight(processed_code, self.line_range)
642 if not self.line_numbers and not self.word_wrap and not self.line_range:
643 if not ends_on_nl:
644 text.remove_suffix("\n")
645 # Simple case of just rendering text
646 style = (
647 self._get_base_style()
648 + self._theme.get_style_for_token(Comment)
649 + Style(dim=True)
650 + self.background_style
651 )
652 if self.indent_guides and not options.ascii_only:
653 text = text.with_indent_guides(self.tab_size, style=style)
654 text.overflow = "crop"
655 if style.transparent_background:
656 yield from console.render(
657 text, options=options.update(width=code_width)
658 )
659 else:
660 syntax_lines = console.render_lines(
661 text,
662 options.update(width=code_width, height=None, justify="left"),
663 style=self.background_style,
664 pad=True,
665 new_lines=True,
666 )
667 for syntax_line in syntax_lines:
668 yield from syntax_line
669 return
671 start_line, end_line = self.line_range or (None, None)
672 line_offset = 0
673 if start_line:
674 line_offset = max(0, start_line - 1)
675 lines: Union[List[Text], Lines] = text.split("\n", allow_blank=ends_on_nl)
676 if self.line_range:
677 if line_offset > len(lines):
678 return
679 lines = lines[line_offset:end_line]
681 if self.indent_guides and not options.ascii_only:
682 style = (
683 self._get_base_style()
684 + self._theme.get_style_for_token(Comment)
685 + Style(dim=True)
686 + self.background_style
687 )
688 lines = (
689 Text("\n")
690 .join(lines)
691 .with_indent_guides(self.tab_size, style=style)
692 .split("\n", allow_blank=True)
693 )
695 numbers_column_width = self._numbers_column_width
696 render_options = options.update(width=code_width)
698 highlight_line = self.highlight_lines.__contains__
699 _Segment = Segment
700 new_line = _Segment("\n")
702 line_pointer = "> " if options.legacy_windows else "❱ "
704 (
705 background_style,
706 number_style,
707 highlight_number_style,
708 ) = self._get_number_styles(console)
710 for line_no, line in enumerate(lines, self.start_line + line_offset):
711 if self.word_wrap:
712 wrapped_lines = console.render_lines(
713 line,
714 render_options.update(height=None, justify="left"),
715 style=background_style,
716 pad=not transparent_background,
717 )
718 else:
719 segments = list(line.render(console, end=""))
720 if options.no_wrap:
721 wrapped_lines = [segments]
722 else:
723 wrapped_lines = [
724 _Segment.adjust_line_length(
725 segments,
726 render_options.max_width,
727 style=background_style,
728 pad=not transparent_background,
729 )
730 ]
732 if self.line_numbers:
733 wrapped_line_left_pad = _Segment(
734 " " * numbers_column_width + " ", background_style
735 )
736 for first, wrapped_line in loop_first(wrapped_lines):
737 if first:
738 line_column = str(line_no).rjust(numbers_column_width - 2) + " "
739 if highlight_line(line_no):
740 yield _Segment(line_pointer, Style(color="red"))
741 yield _Segment(line_column, highlight_number_style)
742 else:
743 yield _Segment(" ", highlight_number_style)
744 yield _Segment(line_column, number_style)
745 else:
746 yield wrapped_line_left_pad
747 yield from wrapped_line
748 yield new_line
749 else:
750 for wrapped_line in wrapped_lines:
751 yield from wrapped_line
752 yield new_line
754 def _apply_stylized_ranges(self, text: Text) -> None:
755 """
756 Apply stylized ranges to a text instance,
757 using the given code to determine the right portion to apply the style to.
759 Args:
760 text (Text): Text instance to apply the style to.
761 """
762 code = text.plain
763 newlines_offsets = [
764 # Let's add outer boundaries at each side of the list:
765 0,
766 # N.B. using "\n" here is much faster than using metacharacters such as "^" or "\Z":
767 *[
768 match.start() + 1
769 for match in re.finditer("\n", code, flags=re.MULTILINE)
770 ],
771 len(code) + 1,
772 ]
774 for stylized_range in self._stylized_ranges:
775 start = _get_code_index_for_syntax_position(
776 newlines_offsets, stylized_range.start
777 )
778 end = _get_code_index_for_syntax_position(
779 newlines_offsets, stylized_range.end
780 )
781 if start is not None and end is not None:
782 text.stylize(stylized_range.style, start, end)
784 def _process_code(self, code: str) -> Tuple[bool, str]:
785 """
786 Applies various processing to a raw code string
787 (normalises it so it always ends with a line return, dedents it if necessary, etc.)
789 Args:
790 code (str): The raw code string to process
792 Returns:
793 Tuple[bool, str]: the boolean indicates whether the raw code ends with a line return,
794 while the string is the processed code.
795 """
796 ends_on_nl = code.endswith("\n")
797 processed_code = code if ends_on_nl else code + "\n"
798 processed_code = (
799 textwrap.dedent(processed_code) if self.dedent else processed_code
800 )
801 processed_code = processed_code.expandtabs(self.tab_size)
802 return ends_on_nl, processed_code
805def _get_code_index_for_syntax_position(
806 newlines_offsets: Sequence[int], position: SyntaxPosition
807) -> Optional[int]:
808 """
809 Returns the index of the code string for the given positions.
811 Args:
812 newlines_offsets (Sequence[int]): The offset of each newline character found in the code snippet.
813 position (SyntaxPosition): The position to search for.
815 Returns:
816 Optional[int]: The index of the code string for this position, or `None`
817 if the given position's line number is out of range (if it's the column that is out of range
818 we silently clamp its value so that it reaches the end of the line)
819 """
820 lines_count = len(newlines_offsets)
822 line_number, column_index = position
823 if line_number > lines_count or len(newlines_offsets) < (line_number + 1):
824 return None # `line_number` is out of range
825 line_index = line_number - 1
826 line_length = newlines_offsets[line_index + 1] - newlines_offsets[line_index] - 1
827 # If `column_index` is out of range: let's silently clamp it:
828 column_index = min(line_length, column_index)
829 return newlines_offsets[line_index] + column_index
832if __name__ == "__main__": # pragma: no cover
834 import argparse
835 import sys
837 parser = argparse.ArgumentParser(
838 description="Render syntax to the console with Rich"
839 )
840 parser.add_argument(
841 "path",
842 metavar="PATH",
843 help="path to file, or - for stdin",
844 )
845 parser.add_argument(
846 "-c",
847 "--force-color",
848 dest="force_color",
849 action="store_true",
850 default=None,
851 help="force color for non-terminals",
852 )
853 parser.add_argument(
854 "-i",
855 "--indent-guides",
856 dest="indent_guides",
857 action="store_true",
858 default=False,
859 help="display indent guides",
860 )
861 parser.add_argument(
862 "-l",
863 "--line-numbers",
864 dest="line_numbers",
865 action="store_true",
866 help="render line numbers",
867 )
868 parser.add_argument(
869 "-w",
870 "--width",
871 type=int,
872 dest="width",
873 default=None,
874 help="width of output (default will auto-detect)",
875 )
876 parser.add_argument(
877 "-r",
878 "--wrap",
879 dest="word_wrap",
880 action="store_true",
881 default=False,
882 help="word wrap long lines",
883 )
884 parser.add_argument(
885 "-s",
886 "--soft-wrap",
887 action="store_true",
888 dest="soft_wrap",
889 default=False,
890 help="enable soft wrapping mode",
891 )
892 parser.add_argument(
893 "-t", "--theme", dest="theme", default="monokai", help="pygments theme"
894 )
895 parser.add_argument(
896 "-b",
897 "--background-color",
898 dest="background_color",
899 default=None,
900 help="Override background color",
901 )
902 parser.add_argument(
903 "-x",
904 "--lexer",
905 default=None,
906 dest="lexer_name",
907 help="Lexer name",
908 )
909 parser.add_argument(
910 "-p", "--padding", type=int, default=0, dest="padding", help="Padding"
911 )
912 parser.add_argument(
913 "--highlight-line",
914 type=int,
915 default=None,
916 dest="highlight_line",
917 help="The line number (not index!) to highlight",
918 )
919 args = parser.parse_args()
921 from pip._vendor.rich.console import Console
923 console = Console(force_terminal=args.force_color, width=args.width)
925 if args.path == "-":
926 code = sys.stdin.read()
927 syntax = Syntax(
928 code=code,
929 lexer=args.lexer_name,
930 line_numbers=args.line_numbers,
931 word_wrap=args.word_wrap,
932 theme=args.theme,
933 background_color=args.background_color,
934 indent_guides=args.indent_guides,
935 padding=args.padding,
936 highlight_lines={args.highlight_line},
937 )
938 else:
939 syntax = Syntax.from_path(
940 args.path,
941 lexer=args.lexer_name,
942 line_numbers=args.line_numbers,
943 word_wrap=args.word_wrap,
944 theme=args.theme,
945 background_color=args.background_color,
946 indent_guides=args.indent_guides,
947 padding=args.padding,
948 highlight_lines={args.highlight_line},
949 )
950 console.print(syntax, soft_wrap=args.soft_wrap)